From da8fb57bfba0035a1f36c8caae2deee92b00bf83 Mon Sep 17 00:00:00 2001 From: Rafhan Mazaya Fathurrahman Date: Tue, 30 Jun 2026 11:07:50 +0700 Subject: [PATCH] Initialize project and add Netify API integration with TDD workflow --- .gitignore | 4 + backend/database.js | 1079 +++++++++++ backend/netify.js | 1476 +++++++++++++++ backend/routes/dashboard.js | 293 +++ backend/scheduler.js | 282 +++ backend/server.js | 32 + backend/tests/netify.test.js | 125 ++ next.config.ts | 9 +- package-lock.json | 2086 ++++++++++++++++++++- package.json | 23 +- scratch_fix.js | 14 + scratch_test.js | 35 + src/app/agents/page.tsx | 250 +++ src/app/api/[...route]/route.ts | 70 + src/app/apps/page.tsx | 114 ++ src/app/devices/page.tsx | 64 + src/app/dns/page.tsx | 64 + src/app/dpi-analytics/page.tsx | 217 +++ src/app/events/page.tsx | 93 + src/app/flows/page.tsx | 71 + src/app/geography/page.tsx | 101 + src/app/globals.css | 79 +- src/app/intelligence/page.tsx | 252 +++ src/app/layout.tsx | 22 +- src/app/lookup/page.tsx | 189 ++ src/app/network-infrastructure/page.tsx | 225 +++ src/app/network-intelligence/page.tsx | 119 ++ src/app/page.tsx | 353 +++- src/app/security-audit/page.tsx | 192 ++ src/app/threats/page.tsx | 78 + src/components/layout/DashboardLayout.tsx | 40 + src/components/layout/Header.tsx | 41 + src/components/layout/Sidebar.tsx | 96 + src/components/ui/Badge.tsx | 26 + src/components/ui/Card.tsx | 68 + src/components/ui/DataTable.tsx | 102 + src/components/ui/Modal.tsx | 61 + src/components/ui/Tabs.tsx | 49 + src/components/ui/WorldMap.tsx | 141 ++ src/lib/actions/agents.ts | 152 ++ src/lib/api-advanced.ts | 85 + src/lib/api.ts | 460 +++++ src/lib/utils.ts | 13 + 43 files changed, 9230 insertions(+), 115 deletions(-) create mode 100644 backend/database.js create mode 100644 backend/netify.js create mode 100644 backend/routes/dashboard.js create mode 100644 backend/scheduler.js create mode 100644 backend/server.js create mode 100644 backend/tests/netify.test.js create mode 100644 scratch_fix.js create mode 100644 scratch_test.js create mode 100644 src/app/agents/page.tsx create mode 100644 src/app/api/[...route]/route.ts create mode 100644 src/app/apps/page.tsx create mode 100644 src/app/devices/page.tsx create mode 100644 src/app/dns/page.tsx create mode 100644 src/app/dpi-analytics/page.tsx create mode 100644 src/app/events/page.tsx create mode 100644 src/app/flows/page.tsx create mode 100644 src/app/geography/page.tsx create mode 100644 src/app/intelligence/page.tsx create mode 100644 src/app/lookup/page.tsx create mode 100644 src/app/network-infrastructure/page.tsx create mode 100644 src/app/network-intelligence/page.tsx create mode 100644 src/app/security-audit/page.tsx create mode 100644 src/app/threats/page.tsx create mode 100644 src/components/layout/DashboardLayout.tsx create mode 100644 src/components/layout/Header.tsx create mode 100644 src/components/layout/Sidebar.tsx create mode 100644 src/components/ui/Badge.tsx create mode 100644 src/components/ui/Card.tsx create mode 100644 src/components/ui/DataTable.tsx create mode 100644 src/components/ui/Modal.tsx create mode 100644 src/components/ui/Tabs.tsx create mode 100644 src/components/ui/WorldMap.tsx create mode 100644 src/lib/actions/agents.ts create mode 100644 src/lib/api-advanced.ts create mode 100644 src/lib/api.ts create mode 100644 src/lib/utils.ts diff --git a/.gitignore b/.gitignore index 5ef6a52..5bbfa93 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,7 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# local databases & temporary files +backend/netify_data.db* +temp.json diff --git a/backend/database.js b/backend/database.js new file mode 100644 index 0000000..e999f24 --- /dev/null +++ b/backend/database.js @@ -0,0 +1,1079 @@ +// backend/database.js +const Database = require('better-sqlite3'); +const path = require('path'); + +const DB_PATH = path.join(__dirname, 'netify_data.db'); +let db; + +function getDB() { + if (!db) { + db = new Database(DB_PATH); + db.pragma('journal_mode = WAL'); + db.pragma('synchronous = NORMAL'); + initSchema(); + } + return db; +} + +function initSchema() { + const d = getDB(); + + d.exec(`CREATE TABLE IF NOT EXISTS tls_versions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + tls_version TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS tls_ciphers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + tls_cipher TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS tls_security ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + tls_security TEXT, + color TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS netbios_hostnames ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + hostname TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + // ─── DPI Fields 12-21 ────────────────────────────────────────────────────── + + d.exec(`CREATE TABLE IF NOT EXISTS dhcp_fingerprints ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + fingerprint TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS http_user_agents ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + user_agent TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS sni_hostnames ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + sni_hostname TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS ssl_server_cn ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + ssl_server_cn TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS quic_hostnames ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + quic_hostname TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS bittorrent_hashes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + info_hash TEXT, + label TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS ssh_versions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + ssh_version TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS mdns_hostnames ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + mdns_hostname TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + // ─── Intelligence API 22-30 ──────────────────────────────────────────────── + + d.exec(`CREATE TABLE IF NOT EXISTS intel_crypto_mining ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + detected_at TEXT, + ip_address TEXT, mac_address TEXT, + pool_host TEXT, pool_ip TEXT, + protocol TEXT, app_label TEXT, + confidence REAL, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS intel_device_discovery ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + detected_at TEXT, + ip_address TEXT, mac_address TEXT, + device_label TEXT, device_type TEXT, + os_label TEXT, manufacturer TEXT, + is_new INTEGER DEFAULT 1 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS intel_encryption_audit ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + detected_at TEXT, + ip_address TEXT, mac_address TEXT, + device_label TEXT, + encrypted_pct REAL, + unencrypted INTEGER DEFAULT 0, + encrypted INTEGER DEFAULT 0, + total INTEGER DEFAULT 0, + risk_level TEXT + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS intel_insecure_protocols ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + detected_at TEXT, + protocol TEXT, + ip_address TEXT, mac_address TEXT, + dst_ip TEXT, dst_port INTEGER, + app_label TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + risk TEXT, + source TEXT DEFAULT 'api' + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS intel_ip_reputation ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + detected_at TEXT, + ip_address TEXT, local_ip TEXT, + mac_address TEXT, reputation TEXT, + score REAL, country TEXT, + app_label TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + blacklisted INTEGER DEFAULT 1 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS intel_server_discovery ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + detected_at TEXT, + ip_address TEXT, mac_address TEXT, + server_type TEXT, hostname TEXT, + port INTEGER, protocol TEXT, + os_label TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS intel_tor_detection ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + detected_at TEXT, + ip_address TEXT, mac_address TEXT, + exit_node TEXT, circuit_id TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + country TEXT + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS intel_unencrypted_passwords ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + detected_at TEXT, + ip_address TEXT, mac_address TEXT, + dst_ip TEXT, dst_port INTEGER, + protocol TEXT, username TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + severity TEXT DEFAULT 'Critical' + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS intel_vpn_detection ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + detected_at TEXT, + ip_address TEXT, mac_address TEXT, + vpn_type TEXT, remote_ip TEXT, + protocol TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + country TEXT, confidence REAL + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS discovery_os ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + os_label TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + // Tabel baru fitur 1-11 + d.exec(`CREATE TABLE IF NOT EXISTS app_categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + category_label TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS continents ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + continent_name TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS regions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + region_name TEXT, region_code TEXT, + country_name TEXT, country_code TEXT, + download INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS cities ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + city_name TEXT, region_name TEXT, + country_name TEXT, country_code TEXT, + download INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS vlans ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + vlan_id INTEGER, vlan_label TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS interfaces ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + iface_id INTEGER, iface_name TEXT, + iface_role TEXT, agent_id TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS flow_types ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + flow_type_label TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS flow_origins ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + flow_origin_label TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS ip_versions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + ip_version_label TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS remote_ips ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + remote_ip TEXT, ip_version INTEGER, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS mac_bandwidth ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + mac_address TEXT, manufacturer TEXT, + download INTEGER DEFAULT 0, + upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS bandwidth_apps ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + app_id INTEGER, app_label TEXT, app_tag TEXT, category TEXT, + favicon TEXT, download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, + total INTEGER DEFAULT 0, flow_count INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS devices ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + mac_address TEXT, ip_address TEXT, device_label TEXT, + device_type TEXT, os_label TEXT, manufacturer TEXT, + download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, last_seen TEXT + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS flows ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + flow_id TEXT, src_ip TEXT, src_mac TEXT, + dst_ip TEXT, dst_port INTEGER, protocol TEXT, + app_label TEXT, domain TEXT, + bytes_download INTEGER DEFAULT 0, bytes_upload INTEGER DEFAULT 0, + first_seen TEXT, last_seen TEXT + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS threats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + threat_id TEXT, threat_type TEXT, severity TEXT, + mac_address TEXT, ip_address TEXT, dst_ip TEXT, + app_label TEXT, domain TEXT, description TEXT, detected_at TEXT + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS bandwidth_protocols ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + protocol_id INTEGER, protocol_label TEXT, + download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, + flow_count INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS bandwidth_countries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + country_code TEXT, country_name TEXT, + download INTEGER DEFAULT 0, upload INTEGER DEFAULT 0, + flow_count INTEGER DEFAULT 0 + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS dns_queries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + domain TEXT, query_count INTEGER DEFAULT 0, + app_label TEXT, category TEXT + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + event_id TEXT, event_type TEXT, severity TEXT, + mac_address TEXT, ip_address TEXT, + description TEXT, event_at TEXT + )`); + + d.exec(`CREATE TABLE IF NOT EXISTS bandwidth_timeline ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetched_at TEXT NOT NULL, + total_download INTEGER DEFAULT 0, total_upload INTEGER DEFAULT 0, + total_flows INTEGER DEFAULT 0, active_devices INTEGER DEFAULT 0 + )`); + + console.log('[DB] Schema siap.'); +} + +// ─── INSERT FUNCTIONS ───────────────────────────────────────────────────────── + +function insertBandwidthApps(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(` + INSERT INTO bandwidth_apps + (fetched_at, app_id, app_label, app_tag, category, favicon, download, upload, total, flow_count) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + d.transaction((items) => { + for (const r of items) { + stmt.run( + fetchedAt, + r.app_id ?? null, + r.app_label ?? 'Unknown', + r.app_tag ?? null, + r.category ?? null, + r.favicon ?? null, + r.download ?? 0, + r.upload ?? 0, + r.total ?? (r.download ?? 0) + (r.upload ?? 0), + r.flows ?? 0 + ); + } + })(rows); +} + +function insertDevices(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(` + INSERT INTO devices + (fetched_at, mac_address, ip_address, device_label, device_type, os_label, manufacturer, download, upload, last_seen) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + d.transaction((items) => { + for (const r of items) { + stmt.run( + fetchedAt, + r.mac_address ?? null, + r.ip_address ?? null, + r.device_label ?? r.ip_address ?? 'Unknown', + r.device_type ?? null, + r.os_label ?? null, + r.manufacturer ?? null, + r.download ?? 0, + r.upload ?? 0, + r.last_seen ?? fetchedAt + ); + } + })(rows); +} + +function insertFlows(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(` + INSERT INTO flows + (fetched_at, flow_id, src_ip, src_mac, dst_ip, dst_port, protocol, app_label, domain, bytes_download, bytes_upload, first_seen, last_seen) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + d.transaction((items) => { + for (const r of items) { + stmt.run( + fetchedAt, + r.flow_id ?? null, + r.src_ip ?? null, + r.src_mac ?? null, + r.dst_ip ?? null, + r.dst_port ?? null, + r.protocol ?? null, + r.app_label ?? null, + r.domain ?? null, + r.download ?? 0, + r.upload ?? 0, + r.first_seen ?? fetchedAt, + r.last_seen ?? fetchedAt + ); + } + })(rows); +} + +function insertThreats(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(` + INSERT INTO threats + (fetched_at, threat_id, threat_type, severity, mac_address, ip_address, dst_ip, app_label, domain, description, detected_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + d.transaction((items) => { + for (const r of items) { + stmt.run( + fetchedAt, + r.threat_id ?? null, + r.threat_type ?? null, + r.severity ?? null, + r.mac_address ?? null, + r.ip_address ?? null, + r.dst_ip ?? null, + r.app_label ?? null, + r.domain ?? null, + r.description ?? null, + r.detected_at ?? fetchedAt + ); + } + })(rows); +} + +function insertProtocols(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(` + INSERT INTO bandwidth_protocols + (fetched_at, protocol_id, protocol_label, download, upload, flow_count) + VALUES (?, ?, ?, ?, ?, ?) + `); + d.transaction((items) => { + for (const r of items) { + // Data sudah di-flatten oleh netify.js — akses langsung tanpa nested + stmt.run( + fetchedAt, + r.protocol_id ?? null, + r.protocol_label ?? 'Unknown', + r.download ?? 0, + r.upload ?? 0, + r.flows ?? 0 + ); + } + })(rows); +} + +function insertCountries(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(` + INSERT INTO bandwidth_countries + (fetched_at, country_code, country_name, download, upload, flow_count) + VALUES (?, ?, ?, ?, ?, ?) + `); + d.transaction((items) => { + for (const r of items) { + // Data sudah di-flatten oleh netify.js — akses langsung tanpa nested + stmt.run( + fetchedAt, + r.country_code ?? null, + r.country_name ?? 'Unknown', + r.download ?? 0, + r.upload ?? 0, + r.flows ?? 0 + ); + } + })(rows); +} + +function insertDNS(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(` + INSERT INTO dns_queries (fetched_at, domain, query_count, app_label, category) + VALUES (?, ?, ?, ?, ?) + `); + d.transaction((items) => { + for (const r of items) { + stmt.run( + fetchedAt, + r.domain ?? null, + r.query_count ?? 0, + r.app_label ?? null, + r.category ?? null + ); + } + })(rows); +} + +function insertEvents(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(` + INSERT INTO events + (fetched_at, event_id, event_type, severity, mac_address, ip_address, description, event_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `); + d.transaction((items) => { + for (const r of items) { + stmt.run( + fetchedAt, + r.event_id ?? null, + r.event_type ?? null, + r.severity ?? null, + r.mac_address ?? null, + r.ip_address ?? null, + r.description ?? null, + r.event_at ?? fetchedAt + ); + } + })(rows); +} + +function insertBandwidthTimeline(summary, fetchedAt) { + const d = getDB(); + d.prepare(` + INSERT INTO bandwidth_timeline (fetched_at, total_download, total_upload, total_flows, active_devices) + VALUES (?, ?, ?, ?, ?) + `).run(fetchedAt, summary.download ?? 0, summary.upload ?? 0, summary.flows ?? 0, summary.devices ?? 0); +} + +// ─── QUERY FUNCTIONS ────────────────────────────────────────────────────────── + +function getLatestBandwidthApps(limit = 20) { + const d = getDB(); + const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_apps`).get(); + if (!latest?.t) return []; + return d.prepare(` + SELECT * FROM bandwidth_apps WHERE fetched_at = ? ORDER BY download DESC LIMIT ? + `).all(latest.t, limit); +} + +function getLatestDevices(limit = 100) { + const d = getDB(); + const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM devices`).get(); + if (!latest?.t) return []; + return d.prepare(` + SELECT * FROM devices WHERE fetched_at = ? ORDER BY download DESC LIMIT ? + `).all(latest.t, limit); +} + +function getLatestFlows(limit = 100) { + const d = getDB(); + const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get(); + if (!latest?.t) return []; + return d.prepare(` + SELECT * FROM flows WHERE fetched_at = ? ORDER BY bytes_download DESC LIMIT ? + `).all(latest.t, limit); +} + +function getLatestThreats(limit = 50) { + const d = getDB(); + return d.prepare(`SELECT * FROM threats ORDER BY fetched_at DESC LIMIT ?`).all(limit); +} + +function getLatestProtocols() { + const d = getDB(); + const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_protocols`).get(); + if (!latest?.t) return []; + return d.prepare(` + SELECT * FROM bandwidth_protocols WHERE fetched_at = ? ORDER BY download DESC LIMIT 10 + `).all(latest.t); +} + +function getLatestCountries() { + const d = getDB(); + const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_countries`).get(); + if (!latest?.t) return []; + return d.prepare(` + SELECT * FROM bandwidth_countries WHERE fetched_at = ? ORDER BY download DESC LIMIT 15 + `).all(latest.t); +} + +function getLatestDNS(limit = 20) { + const d = getDB(); + const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM dns_queries`).get(); + if (!latest?.t) return []; + return d.prepare(` + SELECT * FROM dns_queries WHERE fetched_at = ? ORDER BY query_count DESC LIMIT ? + `).all(latest.t, limit); +} + +function getLatestEvents(limit = 50) { + const d = getDB(); + return d.prepare(`SELECT * FROM events ORDER BY fetched_at DESC LIMIT ?`).all(limit); +} + +function getBandwidthTimeline(points = 60) { + const d = getDB(); + return d.prepare(` + SELECT * FROM bandwidth_timeline ORDER BY fetched_at DESC LIMIT ? + `).all(points).reverse(); +} + +function getStats() { + const d = getDB(); + + // Hitung devices aktif dari polling terakhir (bukan DISTINCT mac karena null) + const latestDevFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM devices`).get(); + const totalDevices = latestDevFetch?.t + ? (d.prepare(`SELECT COUNT(*) as n FROM devices WHERE fetched_at = ?`).get(latestDevFetch.t)?.n ?? 0) + : 0; + + // Hitung flows aktif dari polling terakhir + const latestFlowFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM flows`).get(); + const activeFlows = latestFlowFetch?.t + ? (d.prepare(`SELECT COUNT(*) as n FROM flows WHERE fetched_at = ?`).get(latestFlowFetch.t)?.n ?? 0) + : 0; + + const totalThreats = d.prepare(`SELECT COUNT(*) as n FROM threats`).get()?.n ?? 0; + const totalEvents = d.prepare(`SELECT COUNT(*) as n FROM events`).get()?.n ?? 0; + const lastFetch = d.prepare(`SELECT MAX(fetched_at) as t FROM bandwidth_timeline`).get()?.t ?? null; + const latestBw = d.prepare(`SELECT * FROM bandwidth_timeline ORDER BY fetched_at DESC LIMIT 1`).get(); + + return { totalDevices, activeFlows, totalThreats, totalEvents, lastFetch, latestBw }; +} + +// ─── INSERT FITUR BARU 1-11 ─────────────────────────────────────────────────── +function insertAppCategories(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO app_categories (fetched_at, category_label, download, upload, total) VALUES (?,?,?,?,?)`); + d.transaction(items => { for (const r of items) stmt.run(fetchedAt, r.category_label, r.download, r.upload, r.total); })(rows); +} + +function insertContinents(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO continents (fetched_at, continent_name, download, upload, total) VALUES (?,?,?,?,?)`); + d.transaction(items => { for (const r of items) stmt.run(fetchedAt, r.continent_name, r.download, r.upload, r.total); })(rows); +} + +function insertRegions(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO regions (fetched_at, region_name, region_code, country_name, country_code, download) VALUES (?,?,?,?,?,?)`); + d.transaction(items => { for (const r of items) stmt.run(fetchedAt, r.region_name, r.region_code, r.country_name, r.country_code, r.download); })(rows); +} + +function insertCities(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO cities (fetched_at, city_name, region_name, country_name, country_code, download) VALUES (?,?,?,?,?,?)`); + d.transaction(items => { for (const r of items) stmt.run(fetchedAt, r.city_name, r.region_name, r.country_name, r.country_code, r.download); })(rows); +} + +function insertVLANs(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO vlans (fetched_at, vlan_id, vlan_label, download, upload, total) VALUES (?,?,?,?,?,?)`); + d.transaction(items => { for (const r of items) stmt.run(fetchedAt, r.vlan_id, r.vlan_label, r.download, r.upload, r.total); })(rows); +} + +function insertInterfaces(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO interfaces (fetched_at, iface_id, iface_name, iface_role, agent_id, download, upload, total) VALUES (?,?,?,?,?,?,?,?)`); + d.transaction(items => { for (const r of items) stmt.run(fetchedAt, r.iface_id, r.iface_name, r.iface_role, String(r.agent_id ?? ''), r.download, r.upload, r.total); })(rows); +} + +function insertFlowTypes(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO flow_types (fetched_at, flow_type_label, download, upload, total) VALUES (?,?,?,?,?)`); + d.transaction(items => { for (const r of items) stmt.run(fetchedAt, r.flow_type_label, r.download, r.upload, r.total); })(rows); +} + +function insertFlowOrigins(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO flow_origins (fetched_at, flow_origin_label, download, upload, total) VALUES (?,?,?,?,?)`); + d.transaction(items => { for (const r of items) stmt.run(fetchedAt, r.flow_origin_label, r.download, r.upload, r.total); })(rows); +} + +function insertIPVersions(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO ip_versions (fetched_at, ip_version_label, download, upload, total) VALUES (?,?,?,?,?)`); + d.transaction(items => { for (const r of items) stmt.run(fetchedAt, r.ip_version_label, r.download, r.upload, r.total); })(rows); +} + +function insertRemoteIPs(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO remote_ips (fetched_at, remote_ip, ip_version, download, upload, total) VALUES (?,?,?,?,?,?)`); + d.transaction(items => { for (const r of items) stmt.run(fetchedAt, r.remote_ip, r.ip_version, r.download, r.upload, r.total); })(rows); +} + +function insertMACBandwidth(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO mac_bandwidth (fetched_at, mac_address, manufacturer, download, upload, total) VALUES (?,?,?,?,?,?)`); + d.transaction(items => { for (const r of items) stmt.run(fetchedAt, r.mac_address, r.manufacturer, r.download, r.upload, r.total); })(rows); +} + +// ─── QUERY FITUR BARU ───────────────────────────────────────────────────────── +function getLatest(table, orderBy = 'download', limit = 50) { + const d = getDB(); + const latest = d.prepare(`SELECT MAX(fetched_at) as t FROM ${table}`).get(); + if (!latest?.t) return []; + return d.prepare(`SELECT * FROM ${table} WHERE fetched_at = ? ORDER BY ${orderBy} DESC LIMIT ?`).all(latest.t, limit); +} + +// DPI Fields +function insertTLSVersions(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO tls_versions (fetched_at, tls_version, download, upload, total) VALUES (?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run(fetchedAt, r.tls_version, r.download, r.upload, r.total); + })(rows); +} + +function insertTLSCiphers(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO tls_ciphers (fetched_at, tls_cipher, download, upload, total) VALUES (?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run(fetchedAt, r.tls_cipher, r.download, r.upload, r.total); + })(rows); +} + +function insertTLSSecurity(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO tls_security (fetched_at, tls_security, color, download, upload, total) VALUES (?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run(fetchedAt, r.tls_security, r.color, r.download, r.upload, r.total); + })(rows); +} + +function insertNetBIOSHostnames(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO netbios_hostnames (fetched_at, hostname, download, upload, total) VALUES (?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run(fetchedAt, r.hostname, r.download, r.upload, r.total); + })(rows); +} + +function insertDiscoveryOS(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO discovery_os (fetched_at, os_label, download, upload, total) VALUES (?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run(fetchedAt, r.os_label, r.download, r.upload, r.total); + })(rows); +} + +// ─── INSERT DPI 12-21 ──────────────────────────────────────────────────────── + +function insertDHCPFingerprints(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO dhcp_fingerprints (fetched_at, fingerprint, download, upload, total) VALUES (?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run(fetchedAt, r.fingerprint, r.download, r.upload, r.total); + })(rows); +} + +function insertHTTPUserAgents(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO http_user_agents (fetched_at, user_agent, download, upload, total) VALUES (?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run(fetchedAt, r.user_agent, r.download, r.upload, r.total); + })(rows); +} + +function insertSNIHostnames(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO sni_hostnames (fetched_at, sni_hostname, download, upload, total) VALUES (?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run(fetchedAt, r.sni_hostname, r.download, r.upload, r.total); + })(rows); +} + +function insertSSLServerCN(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO ssl_server_cn (fetched_at, ssl_server_cn, download, upload, total) VALUES (?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run(fetchedAt, r.ssl_server_cn, r.download, r.upload, r.total); + })(rows); +} + +function insertQUICHostnames(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO quic_hostnames (fetched_at, quic_hostname, download, upload, total) VALUES (?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run(fetchedAt, r.quic_hostname, r.download, r.upload, r.total); + })(rows); +} + +function insertBitTorrentHashes(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO bittorrent_hashes (fetched_at, info_hash, label, download, upload, total) VALUES (?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run(fetchedAt, r.info_hash, r.label, r.download, r.upload, r.total); + })(rows); +} + +function insertSSHVersions(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO ssh_versions (fetched_at, ssh_version, download, upload, total) VALUES (?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run(fetchedAt, r.ssh_version, r.download, r.upload, r.total); + })(rows); +} + +function insertMDNSHostnames(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO mdns_hostnames (fetched_at, mdns_hostname, download, upload, total) VALUES (?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run(fetchedAt, r.mdns_hostname, r.download, r.upload, r.total); + })(rows); +} + +// ─── INSERT INTELLIGENCE 22-30 ──────────────────────────────────────────────── + +function insertCryptoMining(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_crypto_mining + (fetched_at, detected_at, ip_address, mac_address, pool_host, pool_ip, protocol, app_label, confidence, download, upload) + VALUES (?,?,?,?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, + r.pool_host, r.pool_ip, r.protocol, r.app_label, r.confidence, + r.download ?? 0, r.upload ?? 0 + ); + })(rows); +} + +function insertDeviceDiscovery(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_device_discovery + (fetched_at, detected_at, ip_address, mac_address, device_label, device_type, os_label, manufacturer, is_new) + VALUES (?,?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, + r.device_label, r.device_type, r.os_label, r.manufacturer, + r.is_new ? 1 : 0 + ); + })(rows); +} + +function insertEncryptionAudit(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_encryption_audit + (fetched_at, detected_at, ip_address, mac_address, device_label, encrypted_pct, unencrypted, encrypted, total, risk_level) + VALUES (?,?,?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, + r.device_label, r.encrypted_pct, r.unencrypted ?? 0, r.encrypted ?? 0, + r.total ?? 0, r.risk_level + ); + })(rows); +} + +function insertInsecureProtocols(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_insecure_protocols + (fetched_at, detected_at, protocol, ip_address, mac_address, dst_ip, dst_port, app_label, download, upload, risk, source) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + fetchedAt, r.detected_at ?? fetchedAt, r.protocol, r.ip_address, r.mac_address, + r.dst_ip, r.dst_port, r.app_label, r.download ?? 0, r.upload ?? 0, + r.risk ?? 'Medium', r.source ?? 'api' + ); + })(rows); +} + +function insertIPReputation(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_ip_reputation + (fetched_at, detected_at, ip_address, local_ip, mac_address, reputation, score, country, app_label, download, upload, blacklisted) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.local_ip, r.mac_address, + r.reputation, r.score, r.country, r.app_label, + r.download ?? 0, r.upload ?? 0, r.blacklisted ? 1 : 0 + ); + })(rows); +} + +function insertServerDiscovery(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_server_discovery + (fetched_at, detected_at, ip_address, mac_address, server_type, hostname, port, protocol, os_label, download, upload) + VALUES (?,?,?,?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, + r.server_type, r.hostname, r.port, r.protocol, r.os_label, + r.download ?? 0, r.upload ?? 0 + ); + })(rows); +} + +function insertTorDetection(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_tor_detection + (fetched_at, detected_at, ip_address, mac_address, exit_node, circuit_id, download, upload, country) + VALUES (?,?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, + r.exit_node, r.circuit_id, r.download ?? 0, r.upload ?? 0, r.country + ); + })(rows); +} + +function insertUnencryptedPasswords(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_unencrypted_passwords + (fetched_at, detected_at, ip_address, mac_address, dst_ip, dst_port, protocol, username, download, upload, severity) + VALUES (?,?,?,?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, + r.dst_ip, r.dst_port, r.protocol, r.username, + r.download ?? 0, r.upload ?? 0, r.severity ?? 'Critical' + ); + })(rows); +} + +function insertVPNDetection(rows, fetchedAt) { + const d = getDB(); + const stmt = d.prepare(`INSERT INTO intel_vpn_detection + (fetched_at, detected_at, ip_address, mac_address, vpn_type, remote_ip, protocol, download, upload, country, confidence) + VALUES (?,?,?,?,?,?,?,?,?,?,?)`); + d.transaction(items => { + for (const r of items) stmt.run( + fetchedAt, r.detected_at ?? fetchedAt, r.ip_address, r.mac_address, + r.vpn_type, r.remote_ip, r.protocol, + r.download ?? 0, r.upload ?? 0, r.country, r.confidence + ); + })(rows); +} + +// ─── QUERY Intelligence — ambil semua row tanpa batasan fetched_at ──────────── +// (karena event ini tidak diposting ulang tiap menit, simpan kumulatif) +function getIntelData(table, limit = 100) { + const d = getDB(); + return d.prepare(`SELECT * FROM ${table} ORDER BY fetched_at DESC LIMIT ?`).all(limit); +} + +function getIntelStats() { + const d = getDB(); + const tables = [ + 'intel_crypto_mining', 'intel_device_discovery', 'intel_encryption_audit', + 'intel_insecure_protocols', 'intel_ip_reputation', 'intel_server_discovery', + 'intel_tor_detection', 'intel_unencrypted_passwords', 'intel_vpn_detection', + ]; + const counts = {}; + for (const t of tables) { + try { + counts[t] = d.prepare(`SELECT COUNT(*) as n FROM ${t}`).get()?.n ?? 0; + } catch { counts[t] = 0; } + } + return counts; +} + +module.exports = { + getDB, + insertBandwidthApps, insertDevices, insertFlows, insertThreats, + insertProtocols, insertCountries, insertDNS, insertEvents, + insertBandwidthTimeline, + getLatestBandwidthApps, getLatestDevices, getLatestFlows, getLatestThreats, + getLatestProtocols, getLatestCountries, getLatestDNS, getLatestEvents, + getBandwidthTimeline, getStats, + // Insert baru + insertAppCategories, insertContinents, insertRegions, insertCities, + insertVLANs, insertInterfaces, insertFlowTypes, insertFlowOrigins, + insertIPVersions, insertRemoteIPs, insertMACBandwidth, + // Query helper + getLatest, + insertTLSVersions, insertTLSCiphers, insertTLSSecurity, insertNetBIOSHostnames, + insertDiscoveryOS, + // DPI 12-21 + insertDHCPFingerprints, insertHTTPUserAgents, insertSNIHostnames, insertSSLServerCN, + insertQUICHostnames, insertBitTorrentHashes, insertSSHVersions, insertMDNSHostnames, + // Intelligence 22-30 + insertCryptoMining, insertDeviceDiscovery, insertEncryptionAudit, + insertInsecureProtocols, insertIPReputation, insertServerDiscovery, + insertTorDetection, insertUnencryptedPasswords, insertVPNDetection, + getIntelData, getIntelStats, +}; \ No newline at end of file diff --git a/backend/netify.js b/backend/netify.js new file mode 100644 index 0000000..f258755 --- /dev/null +++ b/backend/netify.js @@ -0,0 +1,1476 @@ +// backend/netify.js +const path = require('path'); +require('dotenv').config({ path: path.join(__dirname, '..', '.env.local') }); +const axios = require('axios'); + +const BASE_URL = 'https://informatics.netify.ai/api/v1'; +const JWT_TOKEN = process.env.NETIFY_JWT_TOKEN; +const SITE_UUID = process.env.NETIFY_SITE_UUID; + +function headersSite(useApiKey = false) { + const token = useApiKey ? process.env.NETIFY_API_KEY : JWT_TOKEN; + const headers = { 'x-api-key': token, 'Accept': 'application/json' }; + if (!useApiKey) { + headers['x-net-site'] = SITE_UUID; + } + return headers; +} + +async function netifyFetch(path, params = {}, useApiKey = false) { + try { + const res = await axios.get(`${BASE_URL}${path}`, { + headers: headersSite(useApiKey), params, timeout: 30000, + }); + const json = res.data; + if (json?.status_code !== 0) { + console.error(`[Netify] API Error ${json?.status_code} pada ${path}: ${json?.status_message}`); + return null; + } + return json?.data ?? null; + } catch (err) { + const s = err.response?.status; + const msg = JSON.stringify(err.response?.data ?? err.message); + console.error(`[Netify] ${s ?? 'ERR'} ${path}: ${msg}`); + return null; + } +} + +// ─── TOP APPS: download + upload digabung ───────────────────────────────────── +async function fetchTopApps(interval = 1440, limit = 20) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/application/download', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/application/upload', { filter_interval: interval, settings_limit: limit }), + ]); + if (!dlData) return null; + + // Buat map upload berdasarkan app_id + 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 => ({ + app_id : r.application?.id ?? null, + app_label : r.application?.label ?? 'Unknown', + app_tag : r.application?.tag ?? null, + category : r.application?.category?.label ?? null, + favicon : r.application?.favicon ?? null, + download : r.download ?? 0, + upload : ulMap[r.application?.id] ?? 0, + total : (r.download ?? 0) + (ulMap[r.application?.id] ?? 0), + flows : r.flows ?? 0, + })); +} + +// ─── TOP DEVICES: download + upload digabung ────────────────────────────────── +async function fetchTopDevices(interval = 1440, limit = 50) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/local_ip/download', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/local_ip/upload', { filter_interval: interval, settings_limit: limit }), + ]); + if (!dlData) return null; + + // Map upload berdasarkan IP address + 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 : null, + device_label : ip, + device_type : null, + os_label : null, + manufacturer : null, + download : r.download ?? 0, + upload : ulMap[ip] ?? 0, + last_seen : null, + }; + }); +} + +// ─── FLOWS: endpoint /data/flows yang sudah confirmed ada ──────────────────── +async function fetchFlows(limit = 500) { + const raw = await netifyFetch('/data/flows', { settings_limit: limit }); + if (!raw || !Array.isArray(raw)) return null; + + return raw.map(r => ({ + flow_id : String(r.flow_id ?? ''), + src_ip : r.local_ip?.address ?? null, + src_mac : r.local_mac ?? r.mac?.address ?? null, + dst_ip : r.remote_ip?.address ?? null, + dst_port : r.remote_port ?? null, + protocol : r.ip_protocol?.label ?? null, + app_label : null, // tidak tersedia di API flows + domain : null, // tidak tersedia di API flows + download : r.download ?? 0, + upload : r.upload ?? 0, + first_seen : r.first_seen_at?.date ?? null, + last_seen : r.last_seen_at?.date ?? null, + })); +} + +// ─── PROTOCOLS ──────────────────────────────────────────────────────────────── +async function fetchTopProtocols(interval = 1440, limit = 20) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/protocol/download', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/protocol/upload', { filter_interval: interval, settings_limit: limit }), + ]); + if (!dlData) return null; + + const ulMap = {}; + if (ulData) { + for (const r of ulData) { + const id = r.protocol?.id; + if (id) ulMap[id] = r.upload ?? 0; + } + } + + return dlData.map(r => ({ + protocol_id : r.protocol?.id ?? null, + protocol_label : r.protocol?.label ?? 'Unknown', + download : r.download ?? 0, + upload : ulMap[r.protocol?.id] ?? 0, + flows : r.flows ?? 0, + })); +} + +// ─── COUNTRIES ──────────────────────────────────────────────────────────────── +async function fetchTopCountries(interval = 1440, limit = 15) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/country/download', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/country/upload', { filter_interval: interval, settings_limit: limit }), + ]); + if (!dlData) return null; + + const ulMap = {}; + if (ulData) { + for (const r of ulData) { + const code = r.country?.code; + if (code) ulMap[code] = r.upload ?? 0; + } + } + + return dlData.map(r => ({ + country_code : r.country?.code ?? null, + country_name : r.country?.label ?? 'Unknown', + download : r.download ?? 0, + upload : ulMap[r.country?.code] ?? 0, + flows : r.flows ?? 0, + })); +} + +// ─── DNS/HOSTNAME ───────────────────────────────────────────────────────────── +async function fetchTopDomains(interval = 1440, limit = 20) { + const raw = await netifyFetch('/data/stats/top/hostname/download', { + filter_interval: interval, settings_limit: limit, + }); + if (!raw) return null; + + return raw.map(r => ({ + domain : r.hostname?.name ?? null, + app_label : r.hostname?.application_name ?? null, + category : r.hostname?.application_category_label ?? null, + download : r.download ?? 0, + query_count : r.flows ?? r.download ?? 0, + })); +} + +// ─── BANDWIDTH SUMMARY untuk timeline ───────────────────────────────────────── +async function fetchBandwidthSummary(interval = 30) { + // Pakai interval PENDEK (30 menit) untuk timeline supaya ada variasi naik turun + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/application/download', { filter_interval: interval, settings_limit: 100 }), + netifyFetch('/data/stats/top/application/upload', { filter_interval: interval, settings_limit: 100 }), + ]); + + const download = (dlData ?? []).reduce((s, r) => s + (r.download ?? 0), 0); + const upload = (ulData ?? []).reduce((s, r) => s + (r.upload ?? 0), 0); + const flows = (dlData ?? []).reduce((s, r) => s + (r.flows ?? 0), 0); + + return { download, upload, flows, devices: dlData?.length ?? 0 }; +} + +// ─── THREATS — gunakan flows dengan filter anomali sebagai fallback ──────────── +async function fetchCyberThreats(limit = 50) { + // Events/threats belum ada di v1 — return array kosong agar tidak error + // Akan diisi saat endpoint ditemukan + return []; +} + +// ─── EVENTS ─────────────────────────────────────────────────────────────────── +async function fetchEvents(limit = 50) { + const raw = await netifyFetch('/event/events', { settings_limit: limit }); + 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 val = Array.isArray(descObj.tags[k]) ? descObj.tags[k][0] : descObj.tags[k]; + 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, + mac_address: r.additional?.device?.mac?.address || null, + ip_address: srcIp, + description: msg, + event_at: r.created_at?.date || new Date().toISOString() + }; + }); +} + +async function fetchDiscoveredDevices(interval = 1440, limit = 500) { + const [intelRaw, dlData, ulData] = await Promise.all([ + netifyFetch('/intelligence/discovery/devices', { settings_limit: limit }), + netifyFetch('/data/stats/top/local_ip/download', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/local_ip/upload', { filter_interval: interval, settings_limit: limit }), + ]); + + if (!intelRaw || !Array.isArray(intelRaw)) return []; + + // Map bandwidth per IP + const dlMap = {}; + const ulMap = {}; + if (dlData) { + for (const r of dlData) { + const ip = r.local_ip?.address ?? String(r.local_ip); + dlMap[ip] = r.download ?? 0; + } + } + if (ulData) { + for (const r of ulData) { + const ip = r.local_ip?.address ?? String(r.local_ip); + ulMap[ip] = r.upload ?? 0; + } + } + + return intelRaw.map(r => { + const ip = r.ip?.address ?? null; + const mac = r.mac_address ?? r.discovery_mac?.address ?? null; + const oui = mac ? mac.substring(0, 8).toUpperCase() : null; + const mfr = r.mac_vendor !== 'Unknown' && r.mac_vendor !== 'Local' ? r.mac_vendor : (OUI_MAP[oui] ?? r.mac_vendor ?? null); + + return { + ip_address : ip, + mac_address : mac, + device_label : r.device?.label || r.discovery_mac?.discovery_hardware || ip || 'Unknown', + device_type : r.discovery_type?.label || null, + os_label : r.discovery_os?.label || null, + manufacturer : mfr, + download : ip ? (dlMap[ip] ?? 0) : 0, + upload : ip ? (ulMap[ip] ?? 0) : 0, + last_seen : r.last_seen_at?.date || r.discovery_mac?.last_seen_at?.date || null, + }; + }).sort((a, b) => b.download - a.download); +} + +// OUI lookup — manufacturer dari 3 oktet pertama MAC +const OUI_MAP = { + '60:BE:B4' : 'Ruckus Networks', + '74:6F:88' : 'Ruckus Networks', + '04:F4:1C' : 'MikroTik', + 'F4:6D:3F' : 'TP-Link', + 'B8:27:EB' : 'Raspberry Pi', + 'DC:A6:32' : 'Raspberry Pi', + 'E4:5F:01' : 'Raspberry Pi', + '00:50:56' : 'VMware', + '08:00:27' : 'VirtualBox', + 'AC:17:02' : 'ASUSTek', + 'B4:2E:99' : 'ASUSTek', + '18:31:BF' : 'ASUSTek', + '74:D0:2B' : 'Cisco', + 'F8:72:EA' : 'Cisco', + '00:1A:A0' : 'Cisco', + 'FC:FB:FB' : 'Cisco Meraki', + '88:15:44' : 'Cisco Meraki', + '00:18:0A' : 'Ubiquiti', + '04:18:D6' : 'Ubiquiti', + '24:A4:3C' : 'Ubiquiti', + '78:8A:20' : 'Ubiquiti', + 'DC:9F:DB' : 'Ubiquiti', + '80:2A:A8' : 'Ubiquiti', + 'FC:EC:DA' : 'Ubiquiti', + '00:27:22' : 'Ubiquiti', + 'B4:FB:E4' : 'Samsung', + '8C:79:F0' : 'Samsung', + 'F0:25:B7' : 'Samsung', + '78:BD:BC' : 'Samsung', + '3C:28:6D' : 'Apple', + 'A4:C3:F0' : 'Apple', + 'F8:FF:C2' : 'Apple', + '98:01:A7' : 'Apple', + '3C:22:FB' : 'Apple', + 'F0:DB:F8' : 'Huawei', + '00:E0:FC' : 'Huawei', + '54:89:98' : 'Huawei', + '28:31:52' : 'Xiaomi', + '64:09:80' : 'Xiaomi', + 'AC:C1:EE' : 'Xiaomi', + '50:64:2B' : 'Xiaomi', + '00:0C:29' : 'VMware', + '00:15:5D' : 'Microsoft Hyper-V', + '52:54:00' : 'QEMU/KVM', +}; + +// ─── TAMBAHAN FITUR 1-11 ────────────────────────────────────────────────────── + +// 1. Top Application Category +async function fetchTopAppCategories(interval = 1440, limit = 15) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/application_category/download', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/application_category/upload', { filter_interval: interval, settings_limit: limit }), + ]); + if (!dlData) return null; + const ulMap = {}; + if (ulData) for (const r of ulData) { + const key = r.application_category?.label ?? r.application_category; + if (key) ulMap[key] = r.upload ?? 0; + } + return dlData.map(r => { + const label = r.application_category?.label ?? String(r.application_category ?? 'Unknown'); + return { + category_label : label, + download : r.download ?? 0, + upload : ulMap[label] ?? 0, + total : (r.download ?? 0) + (ulMap[label] ?? 0), + }; + }); +} + +// 2. Top Continent +async function fetchTopContinents(interval = 1440, limit = 10) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/continent/download', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/continent/upload', { filter_interval: interval, settings_limit: limit }), + ]); + if (!dlData) return null; + const ulMap = {}; + if (ulData) for (const r of ulData) { + const key = r.continent?.label ?? String(r.continent ?? ''); + if (key) ulMap[key] = r.upload ?? 0; + } + return dlData.map(r => { + const label = r.continent?.label ?? String(r.continent ?? 'Unknown'); + return { + continent_name : label, + download : r.download ?? 0, + upload : ulMap[label] ?? 0, + total : (r.download ?? 0) + (ulMap[label] ?? 0), + }; + }); +} + +// 3. Top Region +async function fetchTopRegions(interval = 1440, limit = 20) { + const raw = await netifyFetch('/data/stats/top/region/download', { + filter_interval: interval, settings_limit: limit, + }); + if (!raw) return null; + return raw.map(r => ({ + region_name : r.region?.region_name?.trim() || '(Unknown Region)', + region_code : r.region?.region_code?.trim() || null, + country_name : r.region?.country_name ?? null, + country_code : r.region?.country_code ?? null, + download : r.download ?? 0, + })); +} + +// 4. Top City +async function fetchTopCities(interval = 1440, limit = 20) { + const raw = await netifyFetch('/data/stats/top/city/download', { + filter_interval: interval, settings_limit: limit, + }); + if (!raw) return null; + return raw.map(r => ({ + city_name : r.city?.city?.trim() || '(Unknown City)', + region_name : r.city?.region_name?.trim() || null, + country_name : r.city?.country_name ?? null, + country_code : r.city?.country_code ?? null, + download : r.download ?? 0, + })); +} + +// 5. Top VLAN +async function fetchTopVLANs(interval = 1440, limit = 20) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/vlan/download', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/vlan/upload', { filter_interval: interval, settings_limit: limit }), + ]); + if (!dlData) return null; + const ulMap = {}; + if (ulData) for (const r of ulData) { + const key = r.vlan?.id ?? 0; + ulMap[key] = r.upload ?? 0; + } + return dlData.map(r => ({ + vlan_id : r.vlan?.id ?? 0, + vlan_label : r.vlan?.label ?? `VLAN ${r.vlan?.id ?? 0}`, + download : r.download ?? 0, + upload : ulMap[r.vlan?.id ?? 0] ?? 0, + total : (r.download ?? 0) + (ulMap[r.vlan?.id ?? 0] ?? 0), + })); +} + +// 6. Top Interface +async function fetchTopInterfaces(interval = 1440, limit = 20) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/interface/download', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/interface/upload', { filter_interval: interval, settings_limit: limit }), + ]); + if (!dlData) return null; + const ulMap = {}; + if (ulData) for (const r of ulData) { + const key = r.interface?.id; + if (key !== undefined) ulMap[key] = r.upload ?? 0; + } + return dlData.map(r => ({ + iface_id : r.interface?.id ?? null, + iface_name : r.interface?.name ?? 'Unknown', + iface_role : r.interface?.role ?? null, + agent_id : r.interface?.agent_id ?? null, + download : r.download ?? 0, + upload : ulMap[r.interface?.id] ?? 0, + total : (r.download ?? 0) + (ulMap[r.interface?.id] ?? 0), + })); +} + +// 7. Top Flow Type +async function fetchTopFlowTypes(interval = 1440, limit = 10) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/flow_type/download', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/flow_type/upload', { filter_interval: interval, settings_limit: limit }), + ]); + if (!dlData) return null; + const ulMap = {}; + if (ulData) for (const r of ulData) { + const key = r.flow_type?.label ?? String(r.flow_type ?? ''); + if (key) ulMap[key] = r.upload ?? 0; + } + return dlData.map(r => { + const label = r.flow_type?.label ?? String(r.flow_type ?? 'Unknown'); + return { + flow_type_label : label, + download : r.download ?? 0, + upload : ulMap[label] ?? 0, + total : (r.download ?? 0) + (ulMap[label] ?? 0), + }; + }); +} + +// 8. Top Flow Origin +async function fetchTopFlowOrigins(interval = 1440, limit = 10) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/flow_origin/download', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/flow_origin/upload', { filter_interval: interval, settings_limit: limit }), + ]); + if (!dlData) return null; + const ulMap = {}; + if (ulData) for (const r of ulData) { + const key = r.flow_origin?.label ?? String(r.flow_origin ?? ''); + if (key) ulMap[key] = r.upload ?? 0; + } + return dlData.map(r => { + const label = r.flow_origin?.label ?? String(r.flow_origin ?? 'Unknown'); + return { + flow_origin_label : label, + download : r.download ?? 0, + upload : ulMap[label] ?? 0, + total : (r.download ?? 0) + (ulMap[label] ?? 0), + }; + }); +} + +// 9. Top IP Version +async function fetchTopIPVersions(interval = 1440, limit = 5) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/ip_version/download', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/ip_version/upload', { filter_interval: interval, settings_limit: limit }), + ]); + if (!dlData) return null; + const ulMap = {}; + if (ulData) for (const r of ulData) { + const key = r.ip_version?.label ?? String(r.ip_version ?? ''); + if (key) ulMap[key] = r.upload ?? 0; + } + return dlData.map(r => { + const label = r.ip_version?.label ?? String(r.ip_version ?? 'Unknown'); + return { + ip_version_label : label, + download : r.download ?? 0, + upload : ulMap[label] ?? 0, + total : (r.download ?? 0) + (ulMap[label] ?? 0), + }; + }); +} + +// 10. Top Remote IP +async function fetchTopRemoteIPs(interval = 1440, limit = 20) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/remote_ip/download', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/remote_ip/upload', { filter_interval: interval, settings_limit: limit }), + ]); + if (!dlData) return null; + const ulMap = {}; + if (ulData) for (const r of ulData) { + const key = r.remote_ip?.address ?? String(r.remote_ip ?? ''); + if (key) ulMap[key] = r.upload ?? 0; + } + return dlData.map(r => { + const addr = r.remote_ip?.address ?? String(r.remote_ip ?? 'Unknown'); + return { + remote_ip : addr, + ip_version : r.remote_ip?.version ?? null, + download : r.download ?? 0, + upload : ulMap[addr] ?? 0, + total : (r.download ?? 0) + (ulMap[addr] ?? 0), + }; + }); +} + +// 11. Top Local MAC + Discovery OS +async function fetchTopLocalMACs(interval = 1440, limit = 50) { + const [dlData, ulData, osData] = await Promise.all([ + netifyFetch('/data/stats/top/local_mac/download', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/local_mac/upload', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/discovery_os/download', { filter_interval: interval, settings_limit: 20 }), + ]); + if (!dlData) return null; + const ulMap = {}; + if (ulData) for (const r of ulData) { + if (r.local_mac) ulMap[r.local_mac] = r.upload ?? 0; + } + // OS summary untuk info panel + const osList = (osData ?? []).map(r => ({ + os_label : r.discovery_os?.label ?? String(r.discovery_os ?? 'Unknown'), + download : r.download ?? 0, + })); + return dlData.map(r => { + const mac = r.local_mac ?? 'Unknown'; + const oui = mac.substring(0, 8).toUpperCase(); + return { + mac_address : mac, + manufacturer : OUI_MAP[oui] ?? null, + download : r.download ?? 0, + upload : ulMap[mac] ?? 0, + total : (r.download ?? 0) + (ulMap[mac] ?? 0), + _os_summary : osList, // disertakan di item pertama saja + }; + }); +} + +// ─── DISCOVERY OS (standalone) ─────────────────────────────────────────────── +async function fetchTopDiscoveryOS(interval = 1440, limit = 20) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/discovery_os/download', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/discovery_os/upload', { filter_interval: interval, settings_limit: limit }), + ]); + if (!dlData) return null; + const ulMap = {}; + if (ulData) for (const r of ulData) { + const key = r.discovery_os?.label ?? String(r.discovery_os ?? ''); + if (key) ulMap[key] = r.upload ?? 0; + } + return dlData.map(r => { + const label = r.discovery_os?.label ?? String(r.discovery_os ?? 'Unknown'); + return { + os_label : label, + download : r.download ?? 0, + upload : ulMap[label] ?? 0, + total : (r.download ?? 0) + (ulMap[label] ?? 0), + }; + }); +} + +// ─── DPI FIELDS ─────────────────────────────────────────────────────────────── + +// TLS Version +async function fetchTLSVersions(interval = 1440, limit = 10) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/tls_version/download', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/tls_version/upload', { filter_interval: interval, settings_limit: limit }), + ]); + if (!dlData) return null; + const ulMap = {}; + if (ulData) for (const r of ulData) { + const key = r.tls_version?.label ?? String(r.tls_version ?? ''); + if (key) ulMap[key] = r.upload ?? 0; + } + return dlData.map(r => { + const label = r.tls_version?.label ?? String(r.tls_version ?? 'Unknown'); + return { + tls_version : label, + download : r.download ?? 0, + upload : ulMap[label] ?? 0, + total : (r.download ?? 0) + (ulMap[label] ?? 0), + }; + }); +} + +// TLS Cipher +async function fetchTLSCiphers(interval = 1440, limit = 15) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/tls_cipher/download', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/tls_cipher/upload', { filter_interval: interval, settings_limit: limit }), + ]); + if (!dlData) return null; + const ulMap = {}; + if (ulData) for (const r of ulData) { + const key = r.tls_cipher?.label ?? String(r.tls_cipher ?? ''); + if (key) ulMap[key] = r.upload ?? 0; + } + return dlData.map(r => { + const label = r.tls_cipher?.label ?? String(r.tls_cipher ?? 'Unknown'); + return { + tls_cipher : label, + download : r.download ?? 0, + upload : ulMap[label] ?? 0, + total : (r.download ?? 0) + (ulMap[label] ?? 0), + }; + }); +} + +// TLS Security Level +async function fetchTLSSecurity(interval = 1440, limit = 10) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/tls_security/download', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/tls_security/upload', { filter_interval: interval, settings_limit: limit }), + ]); + if (!dlData) return null; + const ulMap = {}; + if (ulData) for (const r of ulData) { + const key = r.tls_security?.label ?? String(r.tls_security ?? ''); + if (key) ulMap[key] = r.upload ?? 0; + } + return dlData.map(r => { + const label = r.tls_security?.label ?? String(r.tls_security ?? 'Unknown'); + // Assign warna berdasarkan label untuk UI + const color = label === 'Recommended' ? 'green' + : label === 'Secure' ? 'blue' + : label === 'Weak' ? 'orange' + : label === 'Insecure' ? 'red' + : 'gray'; + return { + tls_security : label, + color : color, + download : r.download ?? 0, + upload : ulMap[label] ?? 0, + total : (r.download ?? 0) + (ulMap[label] ?? 0), + }; + }); +} + +// NetBIOS Hostname (nama PC Windows) +async function fetchNetBIOSHostnames(interval = 1440, limit = 30) { + const [dlData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/netbios_hostname/download', { filter_interval: interval, settings_limit: limit }), + netifyFetch('/data/stats/top/netbios_hostname/upload', { filter_interval: interval, settings_limit: limit }), + ]); + if (!dlData) return null; + const ulMap = {}; + if (ulData) for (const r of ulData) { + const key = r.netbios_hostname?.name ?? String(r.netbios_hostname ?? ''); + if (key) ulMap[key] = r.upload ?? 0; + } + return dlData.map(r => { + const name = r.netbios_hostname?.name ?? String(r.netbios_hostname ?? 'Unknown'); + return { + hostname : name, + download : r.download ?? 0, + upload : ulMap[name] ?? 0, + total : (r.download ?? 0) + (ulMap[name] ?? 0), + }; + }); +} + +async function fetchLookupApplications(page = 1, limit = 50, search = '') { + const params = { + settings_page: page, + settings_limit: limit, + }; + if (search) { + params.filter_application = search; + } + try { + const res = await axios.get(`${BASE_URL}/lookup/applications`, { + headers: headersSite(true), params, timeout: 30000, + }); + const json = res.data; + if (json?.status_code !== 0) { + console.error(`[Netify] API Error ${json?.status_code}: ${json?.status_message}`); + return { applications: [], pagination: {} }; + } + return { + applications: json.data || [], + pagination: json.data_info || {} + }; + } catch (err) { + console.error('[Netify] Lookup error:', err.message); + return { applications: [], pagination: {} }; + } +} + +module.exports = { + fetchLookupApplications, + fetchTopApps, fetchTopDevices, fetchTopProtocols, fetchTopCountries, + fetchTopDomains, fetchBandwidthSummary, fetchDiscoveredDevices, + fetchCyberThreats, fetchFlows, fetchEvents, + fetchTopAppCategories, fetchTopContinents, fetchTopRegions, fetchTopCities, + fetchTopVLANs, fetchTopInterfaces, fetchTopFlowTypes, fetchTopFlowOrigins, + fetchTopIPVersions, fetchTopRemoteIPs, fetchTopLocalMACs, + fetchTopDiscoveryOS, + fetchTLSVersions, fetchTLSCiphers, fetchTLSSecurity, fetchNetBIOSHostnames, + // DPI 12-21 (field name sudah diperbaiki sesuai dokumentasi resmi) + fetchDHCPClassFingerprints, + fetchHTTPUserAgents, + fetchSNIHostnames, + fetchSSLServerCN, + fetchQUICHostnames, + fetchBitTorrentInfoHashes, + fetchSSHClients, + fetchSSHServers, + fetchMDNSHostnames, + // Intelligence 22-30 (derive dari data yang tersedia) + fetchCryptoMining, + fetchDeviceDiscovery, + fetchEncryptionAudit, + fetchInsecureProtocols, + fetchIPReputation, + fetchServerDiscovery, + fetchTorDetection, + fetchUnencryptedPasswords, + fetchVPNDetection, +}; + +// ─── DPI FIELDS 12-21 — FIELD NAMES DIPERBAIKI SESUAI DOCS ────────────────── +// Sumber: https://www.netify.ai/documentation/informatics/v2/data/fields +// +// Field yang SALAH sebelumnya → yang BENAR: +// dhcp_fingerprint → dhcp_class +// user_agent → http_useragent +// ssl_cn → https_sni_hostname +// ssl_server_cn → ssl_server_cn ✓ (sudah benar) +// quic_hostname → quic_hostname ✓ (sudah benar, mungkin belum aktif di akun) +// bt_info_hash → bittorrent_info_hash +// ssh_version → ssh_client / ssh_server (2 field terpisah) +// mdns_hostname → mdns_hostname ✓ (sudah benar, mungkin belum aktif di akun) + +// Helper builder untuk DPI single-field +async function _dpiTopField(fieldName, interval, limit) { + const [dl, ul] = await Promise.all([ + netifyFetch(`/data/stats/top/${fieldName}/download`, { filter_interval: interval, settings_limit: limit }), + netifyFetch(`/data/stats/top/${fieldName}/upload`, { filter_interval: interval, settings_limit: limit }), + ]); + if (!dl) return null; + const ulMap = {}; + if (ul) for (const r of ul) { + const key = r[fieldName]?.name ?? r[fieldName]?.label ?? String(r[fieldName] ?? ''); + if (key) ulMap[key] = r.upload ?? 0; + } + return { dl, ulMap }; +} + +// 12. DHCP Class (field: dhcp_class) +async function fetchDHCPClassFingerprints(interval = 1440, limit = 30) { + const res = await _dpiTopField('dhcp_class', interval, limit); + if (!res) return null; + return res.dl.map(r => { + const label = r.dhcp_class?.name ?? r.dhcp_class?.label ?? String(r.dhcp_class ?? 'Unknown'); + return { + fingerprint : label, + download : r.download ?? 0, + upload : res.ulMap[label] ?? 0, + total : (r.download ?? 0) + (res.ulMap[label] ?? 0), + }; + }); +} + +// 13. HTTP User-Agent (field: http_useragent) +async function fetchHTTPUserAgents(interval = 1440, limit = 30) { + const res = await _dpiTopField('http_useragent', interval, limit); + if (!res) return null; + return res.dl.map(r => { + const label = r.http_useragent?.name ?? r.http_useragent?.label ?? String(r.http_useragent ?? 'Unknown'); + return { + user_agent : label, + download : r.download ?? 0, + upload : res.ulMap[label] ?? 0, + total : (r.download ?? 0) + (res.ulMap[label] ?? 0), + }; + }); +} + +// 14. HTTPS SNI Hostname (field: https_sni_hostname) +async function fetchSNIHostnames(interval = 1440, limit = 30) { + const res = await _dpiTopField('https_sni_hostname', interval, limit); + if (!res) return null; + return res.dl.map(r => { + const name = r.https_sni_hostname?.name ?? r.https_sni_hostname?.label ?? String(r.https_sni_hostname ?? 'Unknown'); + return { + sni_hostname : name, + download : r.download ?? 0, + upload : res.ulMap[name] ?? 0, + total : (r.download ?? 0) + (res.ulMap[name] ?? 0), + }; + }); +} + +// 15. SSL Server Common Name (field: ssl_server_cn) +async function fetchSSLServerCN(interval = 1440, limit = 30) { + const res = await _dpiTopField('ssl_server_cn', interval, limit); + if (!res) return null; + return res.dl.map(r => { + const name = r.ssl_server_cn?.name ?? r.ssl_server_cn?.label ?? String(r.ssl_server_cn ?? 'Unknown'); + return { + ssl_server_cn : name, + download : r.download ?? 0, + upload : res.ulMap[name] ?? 0, + total : (r.download ?? 0) + (res.ulMap[name] ?? 0), + }; + }); +} + +// 17. QUIC Hostname (field: quic_hostname) +async function fetchQUICHostnames(interval = 1440, limit = 30) { + const res = await _dpiTopField('quic_hostname', interval, limit); + if (!res) return null; + return res.dl.map(r => { + const name = r.quic_hostname?.name ?? r.quic_hostname?.label ?? String(r.quic_hostname ?? 'Unknown'); + return { + quic_hostname : name, + download : r.download ?? 0, + upload : res.ulMap[name] ?? 0, + total : (r.download ?? 0) + (res.ulMap[name] ?? 0), + }; + }); +} + +// 18. BitTorrent Info Hash (field: bittorrent_info_hash) +async function fetchBitTorrentInfoHashes(interval = 1440, limit = 30) { + const res = await _dpiTopField('bittorrent_info_hash', interval, limit); + if (!res) return null; + return res.dl.map(r => { + const hash = r.bittorrent_info_hash?.hash ?? r.bittorrent_info_hash?.name ?? String(r.bittorrent_info_hash ?? 'Unknown'); + const label = r.bittorrent_info_hash?.label ?? hash; + return { + info_hash : hash, + label : label, + download : r.download ?? 0, + upload : res.ulMap[hash] ?? res.ulMap[label] ?? 0, + total : (r.download ?? 0) + (res.ulMap[hash] ?? res.ulMap[label] ?? 0), + }; + }); +} + +// 19a. SSH Client (field: ssh_client) +async function fetchSSHClients(interval = 1440, limit = 20) { + const res = await _dpiTopField('ssh_client', interval, limit); + if (!res) return null; + return res.dl.map(r => { + const label = r.ssh_client?.name ?? r.ssh_client?.label ?? String(r.ssh_client ?? 'Unknown'); + return { + ssh_version : label, + direction : 'client', + download : r.download ?? 0, + upload : res.ulMap[label] ?? 0, + total : (r.download ?? 0) + (res.ulMap[label] ?? 0), + }; + }); +} + +// 19b. SSH Server (field: ssh_server) +async function fetchSSHServers(interval = 1440, limit = 20) { + const res = await _dpiTopField('ssh_server', interval, limit); + if (!res) return null; + return res.dl.map(r => { + const label = r.ssh_server?.name ?? r.ssh_server?.label ?? String(r.ssh_server ?? 'Unknown'); + return { + ssh_version : label, + direction : 'server', + download : r.download ?? 0, + upload : res.ulMap[label] ?? 0, + total : (r.download ?? 0) + (res.ulMap[label] ?? 0), + }; + }); +} + +// 21. mDNS Hostname (field: mdns_hostname) +async function fetchMDNSHostnames(interval = 1440, limit = 30) { + const res = await _dpiTopField('mdns_hostname', interval, limit); + if (!res) return null; + return res.dl.map(r => { + const name = r.mdns_hostname?.name ?? r.mdns_hostname?.label ?? String(r.mdns_hostname ?? 'Unknown'); + return { + mdns_hostname : name, + download : r.download ?? 0, + upload : res.ulMap[name] ?? 0, + total : (r.download ?? 0) + (res.ulMap[name] ?? 0), + }; + }); +} + +// ─── INTELLIGENCE 22-30 — DERIVE DARI DATA YANG SUDAH ADA ──────────────────── +// Endpoint /intelligence/* dan /events/* semua 404 di akun ini. +// Semua fungsi berikut meng-DERIVE data dari endpoint yang sudah confirmed bekerja: +// /data/stats/top/*, /data/flows +// Ini memberikan data nyata, bukan mock/dummy. + +// 22. Cryptocurrency Mining — derive dari apps dengan nama mengandung "crypto" / "mining" +// + flows ke port mining pool (3333, 4444, 8333, 9999, 14444) +async function fetchCryptoMining(interval = 1440, limit = 50) { + const MINING_POOLS_PORTS = new Set([3333, 4444, 5555, 7777, 8333, 9332, 9999, 14444, 45560, 45700]); + const MINING_APPS = ['bitcoin', 'crypto', 'mining', 'monero', 'ethereum', 'nicehash', 'nanopool', 'f2pool', 'antpool', 'slushpool']; + + const [appData, flowsRaw] = await Promise.all([ + netifyFetch('/data/stats/top/application/download', { filter_interval: interval, settings_limit: 100 }), + netifyFetch('/data/flows', { settings_limit: 500 }), + ]); + + const results = []; + + // Derive dari aplikasi + if (appData) { + for (const r of appData) { + const name = (r.application?.label ?? '').toLowerCase(); + const tag = (r.application?.tag ?? '').toLowerCase(); + if (MINING_APPS.some(k => name.includes(k) || tag.includes(k))) { + results.push({ + detected_at : null, + ip_address : null, + mac_address : null, + pool_host : r.application?.label ?? null, + pool_ip : null, + protocol : null, + app_label : r.application?.label ?? null, + confidence : 0.7, + download : r.download ?? 0, + upload : r.upload ?? 0, + source : 'derived:app', + }); + } + } + } + + // Derive dari flows ke port mining + if (flowsRaw) { + for (const r of flowsRaw) { + const port = r.remote_port ?? 0; + if (MINING_POOLS_PORTS.has(port)) { + results.push({ + detected_at : r.first_seen_at?.date ?? null, + ip_address : r.local_ip?.address ?? null, + mac_address : r.local_mac ?? null, + pool_host : null, + pool_ip : r.remote_ip?.address ?? null, + protocol : r.ip_protocol?.label ?? null, + app_label : null, + confidence : 0.85, + download : r.download ?? 0, + upload : r.upload ?? 0, + source : 'derived:flow', + }); + } + } + } + + return results.slice(0, limit); +} + +// 23. Device Discovery — derive dari flows (perangkat unik dengan MAC) +async function fetchDeviceDiscovery(limit = 100) { + const [flowsRaw, dlData] = await Promise.all([ + netifyFetch('/data/flows', { settings_limit: 500 }), + netifyFetch('/data/stats/top/local_ip/download', { filter_interval: 1440, settings_limit: 200 }), + ]); + + const seen = new Map(); + if (flowsRaw) { + for (const r of flowsRaw) { + const ip = r.local_ip?.address; + const mac = r.local_mac; + if (!ip) continue; + if (!seen.has(ip)) { + seen.set(ip, { + detected_at : r.first_seen_at?.date ?? null, + ip_address : ip, + mac_address : mac ?? null, + device_label : r.device?.label ?? null, + device_type : r.mac?.discovery_hardware ?? null, + os_label : r.discovery_os?.label ?? null, + manufacturer : mac ? (OUI_MAP[mac.substring(0,8).toUpperCase()] ?? null) : null, + is_new : true, + }); + } + } + } + + // Tambah IP yang punya bandwidth tapi tidak ada di flows + if (dlData) { + for (const r of dlData) { + const ip = r.local_ip?.address ?? String(r.local_ip ?? ''); + if (ip && !seen.has(ip)) { + seen.set(ip, { + detected_at : null, + ip_address : ip, + mac_address : null, + device_label : null, + device_type : null, + os_label : null, + manufacturer : null, + is_new : true, + }); + } + } + } + + return Array.from(seen.values()).slice(0, limit); +} + +// 24. Encryption Audit — derive dari TLS security per-IP dari flows +async function fetchEncryptionAudit(limit = 50) { + const [tlsSec, flowsRaw] = await Promise.all([ + netifyFetch('/data/stats/top/tls_security/download', { filter_interval: 1440, settings_limit: 10 }), + netifyFetch('/data/flows', { settings_limit: 500 }), + ]); + + // Hitung per-IP: encrypted vs unencrypted flows + const ipStats = {}; + if (flowsRaw) { + for (const r of flowsRaw) { + const ip = r.local_ip?.address; + const port = r.remote_port ?? 0; + const mac = r.local_mac ?? null; + if (!ip) continue; + if (!ipStats[ip]) ipStats[ip] = { mac, encrypted: 0, unencrypted: 0, total_bytes: 0 }; + const bytes = (r.download ?? 0) + (r.upload ?? 0); + // Port 443, 8443, 465, 993, 995, 22 = encrypted + const isEnc = [443, 8443, 465, 993, 995, 22, 853].includes(port); + if (isEnc) ipStats[ip].encrypted += bytes; + else ipStats[ip].unencrypted += bytes; + ipStats[ip].total_bytes += bytes; + } + } + + return Object.entries(ipStats) + .map(([ip, s]) => { + const total = s.encrypted + s.unencrypted; + const enc_pct = total > 0 ? (s.encrypted / total) * 100 : null; + const risk = enc_pct === null ? null + : enc_pct >= 90 ? 'Low' + : enc_pct >= 60 ? 'Medium' + : enc_pct >= 30 ? 'High' + : 'Critical'; + const oui = s.mac ? s.mac.substring(0,8).toUpperCase() : null; + return { + ip_address : ip, + mac_address : s.mac, + device_label : OUI_MAP[oui] ?? null, + encrypted_pct : enc_pct != null ? Math.round(enc_pct * 10) / 10 : null, + encrypted : s.encrypted, + unencrypted : s.unencrypted, + total : total, + risk_level : risk, + detected_at : null, + }; + }) + .sort((a, b) => (a.encrypted_pct ?? 101) - (b.encrypted_pct ?? 101)) + .slice(0, limit); +} + +// 25. Insecure Protocols — derive dari top protocols (confirmed bekerja) +async function fetchInsecureProtocols(interval = 1440, limit = 50) { + const INSECURE = { + 'HTTP' : { port: 80, risk: 'High' }, + 'FTP' : { port: 21, risk: 'Critical' }, + 'Telnet' : { port: 23, risk: 'Critical' }, + 'SMTP' : { port: 25, risk: 'Medium' }, + 'POP3' : { port: 110, risk: 'Medium' }, + 'IMAP' : { port: 143, risk: 'Medium' }, + 'DNS' : { port: 53, risk: 'Low' }, + 'SNMP' : { port: 161, risk: 'High' }, + 'LDAP' : { port: 389, risk: 'High' }, + 'RDP' : { port: 3389, risk: 'High' }, + 'NTP' : { port: 123, risk: 'Low' }, + 'TFTP' : { port: 69, risk: 'High' }, + 'rsh' : { port: 514, risk: 'Critical' }, + 'rlogin' : { port: 513, risk: 'Critical' }, + }; + + const [protoData, ulData] = await Promise.all([ + netifyFetch('/data/stats/top/protocol/download', { filter_interval: interval, settings_limit: 100 }), + netifyFetch('/data/stats/top/protocol/upload', { filter_interval: interval, settings_limit: 100 }), + ]); + if (!protoData) return []; + + const ulMap = {}; + if (ulData) for (const r of ulData) { + const id = r.protocol?.id; + if (id) ulMap[id] = r.upload ?? 0; + } + + return protoData + .filter(r => INSECURE[r.protocol?.label]) + .map(r => { + const label = r.protocol?.label ?? 'Unknown'; + const info = INSECURE[label]; + return { + protocol : label, + ip_address : null, + mac_address : null, + dst_ip : null, + dst_port : info.port, + app_label : null, + download : r.download ?? 0, + upload : ulMap[r.protocol?.id] ?? 0, + risk : info.risk, + detected_at : null, + source : 'derived', + }; + }) + .slice(0, limit); +} + +// 26. IP Reputation — derive dari top remote_ip + cross-check negara berisiko tinggi +async function fetchIPReputation(limit = 50) { + // Negara dengan risiko tinggi berdasarkan threat intel umum + const HIGH_RISK_COUNTRIES = new Set([ + 'China', 'Russia', 'Iran', 'North Korea', 'Nigeria', 'Romania', + 'Brazil', 'Ukraine', 'Vietnam', 'Indonesia', + ]); + + const [ipData, countryData] = await Promise.all([ + netifyFetch('/data/stats/top/remote_ip/download', { filter_interval: 1440, settings_limit: 100 }), + netifyFetch('/data/stats/top/country/download', { filter_interval: 1440, settings_limit: 50 }), + ]); + + const results = []; + + if (ipData) { + // Tandai IP dari negara berisiko (informasi negara tidak ada per-IP dari API, + // jadi kita pakai country data untuk konteks) + for (const r of ipData) { + const ip = r.remote_ip?.address ?? String(r.remote_ip ?? ''); + if (!ip) continue; + results.push({ + ip_address : ip, + local_ip : null, + mac_address : null, + reputation : 'Unknown', + score : null, + country : null, + app_label : null, + download : r.download ?? 0, + upload : r.upload ?? 0, + detected_at : null, + blacklisted : false, + source : 'derived:top_ip', + }); + } + } + + // Tambah entri untuk negara berisiko yang terdeteksi + if (countryData) { + for (const r of countryData) { + const country = r.country?.label ?? ''; + if (HIGH_RISK_COUNTRIES.has(country)) { + results.push({ + ip_address : null, + local_ip : null, + mac_address : null, + reputation : 'High-Risk Country', + score : 0.7, + country : country, + app_label : null, + download : r.download ?? 0, + upload : r.upload ?? 0, + detected_at : null, + blacklisted : false, + source : 'derived:country', + }); + } + } + } + + return results.slice(0, limit); +} + +// 27. Server Discovery — derive dari flows dengan flow_origin=Server + port well-known server +async function fetchServerDiscovery(limit = 100) { + const SERVER_PORTS = { + 80: 'HTTP', 443: 'HTTPS', 22: 'SSH', 21: 'FTP', 25: 'SMTP', + 110: 'POP3', 143: 'IMAP', 3306: 'MySQL', 5432: 'PostgreSQL', + 6379: 'Redis', 27017: 'MongoDB', 8080: 'HTTP-Alt', 8443: 'HTTPS-Alt', + 53: 'DNS', 3389: 'RDP', 5900: 'VNC', 161: 'SNMP', 123: 'NTP', + 389: 'LDAP', 636: 'LDAPS', 5060: 'SIP', 1194: 'OpenVPN', + }; + + const [flowOriginData, flowsRaw] = await Promise.all([ + netifyFetch('/data/stats/top/flow_origin/download', { filter_interval: 1440, settings_limit: 10 }), + netifyFetch('/data/flows', { settings_limit: 500 }), + ]); + + const serverMap = {}; + if (flowsRaw) { + for (const r of flowsRaw) { + const localIP = r.local_ip?.address; + const localPort = r.local_port ?? r.remote_port; + const proto = r.ip_protocol?.label ?? null; + const mac = r.local_mac ?? null; + if (!localIP) continue; + + // Deteksi server: local device listening di port well-known + const svcName = SERVER_PORTS[localPort] ?? SERVER_PORTS[r.remote_port]; + if (svcName) { + const key = `${localIP}:${localPort ?? r.remote_port}`; + if (!serverMap[key]) { + const oui = mac ? mac.substring(0,8).toUpperCase() : null; + serverMap[key] = { + detected_at : r.first_seen_at?.date ?? null, + ip_address : localIP, + mac_address : mac, + server_type : svcName, + hostname : r.device?.label ?? null, + port : localPort ?? r.remote_port, + protocol : proto, + os_label : null, + download : 0, + upload : 0, + }; + } + serverMap[key].download += r.download ?? 0; + serverMap[key].upload += r.upload ?? 0; + } + } + } + + return Object.values(serverMap) + .sort((a, b) => (b.download + b.upload) - (a.download + a.upload)) + .slice(0, limit); +} + +// 28. Tor Detection — derive dari top remote_ip + app/hostname matching Tor +async function fetchTorDetection(limit = 50) { + const TOR_INDICATORS = ['tor', '.onion', 'torproject', 'torbrowser']; + + const [appData, domainData] = await Promise.all([ + netifyFetch('/data/stats/top/application/download', { filter_interval: 1440, settings_limit: 100 }), + netifyFetch('/data/stats/top/hostname/download', { filter_interval: 1440, settings_limit: 100 }), + ]); + + const results = []; + + if (appData) { + for (const r of appData) { + const name = (r.application?.label ?? '').toLowerCase(); + const tag = (r.application?.tag ?? '').toLowerCase(); + if (TOR_INDICATORS.some(k => name.includes(k) || tag.includes(k))) { + results.push({ + detected_at : null, + ip_address : null, + mac_address : null, + exit_node : null, + circuit_id : null, + download : r.download ?? 0, + upload : r.upload ?? 0, + country : null, + source : 'derived:app', + label : r.application?.label, + }); + } + } + } + + if (domainData) { + for (const r of domainData) { + const host = (r.hostname?.name ?? '').toLowerCase(); + if (TOR_INDICATORS.some(k => host.includes(k))) { + results.push({ + detected_at : null, + ip_address : null, + mac_address : null, + exit_node : null, + circuit_id : null, + download : r.download ?? 0, + upload : 0, + country : null, + source : 'derived:hostname', + label : r.hostname?.name, + }); + } + } + } + + return results.slice(0, limit); +} + +// 29. Unencrypted Passwords — derive dari flows ke port cleartext auth +async function fetchUnencryptedPasswords(limit = 50) { + // Port yang dikenal mengirim kredensial cleartext + const CLEARTEXT_AUTH_PORTS = { + 21 : { protocol: 'FTP', severity: 'Critical' }, + 23 : { protocol: 'Telnet', severity: 'Critical' }, + 25 : { protocol: 'SMTP', severity: 'High' }, + 80 : { protocol: 'HTTP', severity: 'High' }, + 110 : { protocol: 'POP3', severity: 'High' }, + 143 : { protocol: 'IMAP', severity: 'High' }, + 389 : { protocol: 'LDAP', severity: 'Critical' }, + 512 : { protocol: 'rexec', severity: 'Critical' }, + 513 : { protocol: 'rlogin', severity: 'Critical' }, + 514 : { protocol: 'rsh', severity: 'Critical' }, + }; + + const flowsRaw = await netifyFetch('/data/flows', { settings_limit: 500 }); + if (!flowsRaw) return []; + + const seen = new Map(); + for (const r of flowsRaw) { + const port = r.remote_port ?? 0; + const info = CLEARTEXT_AUTH_PORTS[port]; + if (!info) continue; + + const key = `${r.local_ip?.address}:${r.remote_ip?.address}:${port}`; + if (!seen.has(key)) { + seen.set(key, { + detected_at : r.first_seen_at?.date ?? null, + ip_address : r.local_ip?.address ?? null, + mac_address : r.local_mac ?? null, + dst_ip : r.remote_ip?.address ?? null, + dst_port : port, + protocol : info.protocol, + username : null, + download : 0, + upload : 0, + severity : info.severity, + }); + } + seen.get(key).download += r.download ?? 0; + seen.get(key).upload += r.upload ?? 0; + } + + return Array.from(seen.values()) + .sort((a, b) => (b.download + b.upload) - (a.download + a.upload)) + .slice(0, limit); +} + +// 30. VPN Detection — derive dari apps/protocols/hostnames yang mengindikasikan VPN +async function fetchVPNDetection(limit = 50) { + const VPN_APPS = [ + 'openvpn', 'wireguard', 'nordvpn', 'expressvpn', 'surfshark', 'tunnelbear', + 'mullvad', 'protonvpn', 'ipvanish', 'pia', 'private internet access', + 'hotspot shield', 'cyberghost', 'vpn', 'pptp', 'l2tp', 'ipsec', 'sstp', + 'shadowsocks', 'v2ray', 'trojan', 'outline', + ]; + const VPN_PROTOCOLS = new Set(['OpenVPN', 'WireGuard', 'IPSec', 'PPTP', 'L2TP', 'GRE', 'SSTP']); + // Port yang umum digunakan VPN + const VPN_PORTS = new Set([1194, 51820, 500, 4500, 1701, 1723, 8388, 443]); + + const [appData, protoData, flowsRaw] = await Promise.all([ + netifyFetch('/data/stats/top/application/download', { filter_interval: 1440, settings_limit: 100 }), + netifyFetch('/data/stats/top/protocol/download', { filter_interval: 1440, settings_limit: 50 }), + netifyFetch('/data/flows', { settings_limit: 500 }), + ]); + + const results = []; + + if (appData) { + for (const r of appData) { + const name = (r.application?.label ?? '').toLowerCase(); + const tag = (r.application?.tag ?? '').toLowerCase(); + if (VPN_APPS.some(k => name.includes(k) || tag.includes(k))) { + results.push({ + detected_at : null, + ip_address : null, + mac_address : null, + vpn_type : r.application?.label ?? 'Unknown VPN', + remote_ip : null, + protocol : null, + download : r.download ?? 0, + upload : r.upload ?? 0, + country : null, + confidence : 0.9, + source : 'derived:app', + }); + } + } + } + + if (protoData) { + for (const r of protoData) { + const label = r.protocol?.label ?? ''; + if (VPN_PROTOCOLS.has(label)) { + results.push({ + detected_at : null, + ip_address : null, + mac_address : null, + vpn_type : label, + remote_ip : null, + protocol : label, + download : r.download ?? 0, + upload : r.upload ?? 0, + country : null, + confidence : 0.85, + source : 'derived:protocol', + }); + } + } + } + + if (flowsRaw) { + const portSeen = new Set(); + for (const r of flowsRaw) { + const port = r.remote_port ?? 0; + if (VPN_PORTS.has(port) && !portSeen.has(port)) { + portSeen.add(port); + results.push({ + detected_at : r.first_seen_at?.date ?? null, + ip_address : r.local_ip?.address ?? null, + mac_address : r.local_mac ?? null, + vpn_type : `Port ${port}`, + remote_ip : r.remote_ip?.address ?? null, + protocol : r.ip_protocol?.label ?? null, + download : r.download ?? 0, + upload : r.upload ?? 0, + country : null, + confidence : 0.75, + source : 'derived:port', + }); + } + } + } + + return results.slice(0, limit); +} \ No newline at end of file diff --git a/backend/routes/dashboard.js b/backend/routes/dashboard.js new file mode 100644 index 0000000..7289254 --- /dev/null +++ b/backend/routes/dashboard.js @@ -0,0 +1,293 @@ +// backend/routes/dashboard.js +const express = require('express'); +const router = express.Router(); +const db = require('../database'); +const { runPoll } = require('../scheduler'); + +// GET /api/dashboard/summary — kartu ringkasan (top of page) +router.get('/summary', (req, res) => { + const stats = db.getStats(); + const timeline = db.getBandwidthTimeline(1); + const latest = timeline[0] ?? {}; + + res.json({ + ok: true, + data: { + total_devices : stats.totalDevices, + total_threats : stats.totalThreats, + total_events : stats.totalEvents, + last_fetch : stats.lastFetch, + bandwidth_down : latest.total_download ?? 0, + bandwidth_up : latest.total_upload ?? 0, + active_flows : stats.activeFlows, + } + }); +}); + +// GET /api/dashboard/apps — top aplikasi +router.get('/apps', (req, res) => { + const limit = parseInt(req.query.limit ?? 10); + const data = db.getLatestBandwidthApps(limit); + res.json({ ok: true, data }); +}); + +// GET /api/dashboard/devices — daftar device +router.get('/devices', (req, res) => { + const limit = parseInt(req.query.limit ?? 50); + const data = db.getLatestDevices(limit); + res.json({ ok: true, data }); +}); + +// GET /api/dashboard/flows — flow aktif +router.get('/flows', (req, res) => { + const limit = parseInt(req.query.limit ?? 50); + const data = db.getLatestFlows(limit); + res.json({ ok: true, data }); +}); + +// GET /api/dashboard/threats — ancaman keamanan +router.get('/threats', (req, res) => { + const limit = parseInt(req.query.limit ?? 20); + const data = db.getLatestThreats(limit); + res.json({ ok: true, data }); +}); + +// GET /api/dashboard/protocols — top protokol +router.get('/protocols', (req, res) => { + const data = db.getLatestProtocols(); + res.json({ ok: true, data }); +}); + +// GET /api/dashboard/countries — top negara +router.get('/countries', (req, res) => { + const data = db.getLatestCountries(); + res.json({ ok: true, data }); +}); + +// GET /api/dashboard/dns — top DNS queries +router.get('/dns', (req, res) => { + const limit = parseInt(req.query.limit ?? 20); + const data = db.getLatestDNS(limit); + res.json({ ok: true, data }); +}); + +// GET /api/dashboard/events — events terbaru +router.get('/events', (req, res) => { + const limit = parseInt(req.query.limit ?? 20); + const rawData = db.getLatestEvents(limit); + const mappedData = rawData.map(r => ({ + id: r.id, + event_id: r.event_id, + event_type: r.event_type || 'unknown', + severity: r.severity || 'info', + message: r.description || '', + source_ip: r.ip_address || null, + timestamp: r.event_at || r.fetched_at || new Date().toISOString() + })); + res.json({ ok: true, data: mappedData }); +}); + +// GET /api/dashboard/timeline — bandwidth timeline (grafik) +router.get('/timeline', (req, res) => { + const points = parseInt(req.query.points ?? 60); + const data = db.getBandwidthTimeline(points); + res.json({ ok: true, data }); +}); + +// POST /api/dashboard/refresh — trigger manual poll +router.post('/refresh', async (req, res) => { + await runPoll(); + res.json({ ok: true, message: 'Poll berhasil dijalankan.' }); +}); + +// ─── ROUTES FITUR BARU 1-11 ─────────────────────────────────────────────────── +router.get('/app-categories', (req, res) => { + res.json({ ok: true, data: db.getLatest('app_categories', 'download', 15) }); +}); +router.get('/continents', (req, res) => { + res.json({ ok: true, data: db.getLatest('continents', 'download', 10) }); +}); +router.get('/regions', (req, res) => { + res.json({ ok: true, data: db.getLatest('regions', 'download', 20) }); +}); +router.get('/cities', (req, res) => { + res.json({ ok: true, data: db.getLatest('cities', 'download', 20) }); +}); +router.get('/vlans', (req, res) => { + res.json({ ok: true, data: db.getLatest('vlans', 'download', 20) }); +}); +router.get('/interfaces', (req, res) => { + res.json({ ok: true, data: db.getLatest('interfaces', 'download', 20) }); +}); +router.get('/flow-types', (req, res) => { + res.json({ ok: true, data: db.getLatest('flow_types', 'download', 10) }); +}); +router.get('/flow-origins', (req, res) => { + res.json({ ok: true, data: db.getLatest('flow_origins', 'download', 10) }); +}); +router.get('/ip-versions', (req, res) => { + res.json({ ok: true, data: db.getLatest('ip_versions', 'download', 5) }); +}); +router.get('/remote-ips', (req, res) => { + const limit = parseInt(req.query.limit ?? 20); + res.json({ ok: true, data: db.getLatest('remote_ips', 'download', limit) }); +}); +router.get('/mac-bandwidth', (req, res) => { + const limit = parseInt(req.query.limit ?? 50); + res.json({ ok: true, data: db.getLatest('mac_bandwidth', 'download', limit) }); +}); + +// ─── FIX: ROUTES YANG SEBELUMNYA HILANG ────────────────────────────────────── + +// TLS Versions +router.get('/tls-versions', (req, res) => { + res.json({ ok: true, data: db.getLatest('tls_versions', 'download', 10) }); +}); + +// TLS Ciphers +router.get('/tls-ciphers', (req, res) => { + res.json({ ok: true, data: db.getLatest('tls_ciphers', 'download', 15) }); +}); + +// TLS Security Level +router.get('/tls-security', (req, res) => { + res.json({ ok: true, data: db.getLatest('tls_security', 'download', 10) }); +}); + +// NetBIOS Hostnames (Windows devices) +router.get('/netbios', (req, res) => { + const limit = parseInt(req.query.limit ?? 30); + res.json({ ok: true, data: db.getLatest('netbios_hostnames', 'download', limit) }); +}); + +// Discovery OS (sistem operasi yang terdeteksi) +router.get('/discovery-os', (req, res) => { + res.json({ ok: true, data: db.getLatest('discovery_os', 'download', 20) }); +}); + +// ─── ROUTES DPI 12-21 ───────────────────────────────────────────────────────── + +// 12. DHCP Class Fingerprint +router.get('/dhcp-fingerprints', (req, res) => { + const limit = parseInt(req.query.limit ?? 30); + res.json({ ok: true, data: db.getLatest('dhcp_fingerprints', 'download', limit) }); +}); + +// 13. HTTP User-Agent +router.get('/http-user-agents', (req, res) => { + const limit = parseInt(req.query.limit ?? 30); + res.json({ ok: true, data: db.getLatest('http_user_agents', 'download', limit) }); +}); + +// 14. HTTPS SNI Hostname +router.get('/sni-hostnames', (req, res) => { + const limit = parseInt(req.query.limit ?? 30); + res.json({ ok: true, data: db.getLatest('sni_hostnames', 'download', limit) }); +}); + +// 15. SSL Server Common Name +router.get('/ssl-server-cn', (req, res) => { + const limit = parseInt(req.query.limit ?? 30); + res.json({ ok: true, data: db.getLatest('ssl_server_cn', 'download', limit) }); +}); + +// 17. QUIC Hostname +router.get('/quic-hostnames', (req, res) => { + const limit = parseInt(req.query.limit ?? 30); + res.json({ ok: true, data: db.getLatest('quic_hostnames', 'download', limit) }); +}); + +// 18. BitTorrent Info Hash +router.get('/bittorrent-hashes', (req, res) => { + const limit = parseInt(req.query.limit ?? 30); + res.json({ ok: true, data: db.getLatest('bittorrent_hashes', 'download', limit) }); +}); + +// 19. SSH Version +router.get('/ssh-versions', (req, res) => { + const limit = parseInt(req.query.limit ?? 20); + res.json({ ok: true, data: db.getLatest('ssh_versions', 'download', limit) }); +}); + +// 21. mDNS Hostname (Chromecast, Apple TV, etc.) +router.get('/mdns-hostnames', (req, res) => { + const limit = parseInt(req.query.limit ?? 30); + res.json({ ok: true, data: db.getLatest('mdns_hostnames', 'download', limit) }); +}); + +// ─── INTELLIGENCE ROUTES 22-30 ──────────────────────────────────────────────── + +// Stats ringkasan semua intelligence (untuk badge count di tab) +router.get('/intelligence/stats', (req, res) => { + res.json({ ok: true, data: db.getIntelStats() }); +}); + +// 22. Cryptocurrency Mining +router.get('/intelligence/crypto-mining', (req, res) => { + const limit = parseInt(req.query.limit ?? 50); + res.json({ ok: true, data: db.getIntelData('intel_crypto_mining', limit) }); +}); + +// 23. Device Discovery +router.get('/intelligence/device-discovery', (req, res) => { + const limit = parseInt(req.query.limit ?? 100); + res.json({ ok: true, data: db.getIntelData('intel_device_discovery', limit) }); +}); + +// 24. Encryption Audit +router.get('/intelligence/encryption-audit', (req, res) => { + const limit = parseInt(req.query.limit ?? 50); + res.json({ ok: true, data: db.getIntelData('intel_encryption_audit', limit) }); +}); + +// 25. Insecure Protocols +router.get('/intelligence/insecure-protocols', (req, res) => { + const limit = parseInt(req.query.limit ?? 50); + res.json({ ok: true, data: db.getIntelData('intel_insecure_protocols', limit) }); +}); + +// 26. IP Reputation +router.get('/intelligence/ip-reputation', (req, res) => { + const limit = parseInt(req.query.limit ?? 50); + res.json({ ok: true, data: db.getIntelData('intel_ip_reputation', limit) }); +}); + +// 27. Server Discovery +router.get('/intelligence/server-discovery', (req, res) => { + const limit = parseInt(req.query.limit ?? 100); + res.json({ ok: true, data: db.getIntelData('intel_server_discovery', limit) }); +}); + +// 28. Tor Detection +router.get('/intelligence/tor', (req, res) => { + const limit = parseInt(req.query.limit ?? 50); + res.json({ ok: true, data: db.getIntelData('intel_tor_detection', limit) }); +}); + +// 29. Unencrypted Passwords +router.get('/intelligence/unencrypted-passwords', (req, res) => { + const limit = parseInt(req.query.limit ?? 50); + res.json({ ok: true, data: db.getIntelData('intel_unencrypted_passwords', limit) }); +}); + +// 30. VPN Detection +router.get('/intelligence/vpn', (req, res) => { + const limit = parseInt(req.query.limit ?? 50); + res.json({ ok: true, data: db.getIntelData('intel_vpn_detection', limit) }); +}); + +// ─── LOOKUP ROUTES ──────────────────────────────────────────────────────────── +router.get('/lookup/applications', async (req, res) => { + try { + const page = parseInt(req.query.page ?? 1); + const limit = parseInt(req.query.limit ?? 50); + const search = String(req.query.search ?? ''); + const { fetchLookupApplications } = require('../netify'); + const data = await fetchLookupApplications(page, limit, search); + res.json({ ok: true, data }); + } catch (err) { + res.status(500).json({ ok: false, message: err.message }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/scheduler.js b/backend/scheduler.js new file mode 100644 index 0000000..ca3633a --- /dev/null +++ b/backend/scheduler.js @@ -0,0 +1,282 @@ +// backend/scheduler.js +const cron = require('node-cron'); +const netify = require('./netify'); +const db = require('./database'); + +let isRunning = false; + +async function runPoll() { + if (isRunning) { + console.log('[Scheduler] Poll sedang berjalan, skip.'); + return; + } + isRunning = true; + const fetchedAt = new Date().toISOString(); + console.log(`[Scheduler] Mulai polling... (${fetchedAt})`); + + try { + // 1. Top Aplikasi + const apps = await netify.fetchTopApps(1440, 20); + if (apps && Array.isArray(apps)) { + db.insertBandwidthApps(apps, fetchedAt); + console.log(`[Scheduler] OK Apps : ${apps.length} baris`); + } else { + console.log(`[Scheduler] -- Apps : tidak ada data`); + } + + // 2. Top Devices — pakai fetchDiscoveredDevices yg sudah dinormalisasi + const devices = await netify.fetchDiscoveredDevices(1440, 200); + if (devices && Array.isArray(devices)) { + db.insertDevices(devices, fetchedAt); + console.log(`[Scheduler] OK Devices : ${devices.length} baris`); + } else { + console.log(`[Scheduler] -- Devices : tidak ada data`); + } + + // 3. Top Protokol + const protocols = await netify.fetchTopProtocols(1440, 20); + if (protocols && Array.isArray(protocols)) { + db.insertProtocols(protocols, fetchedAt); + console.log(`[Scheduler] OK Protocols : ${protocols.length} baris`); + } else { + console.log(`[Scheduler] -- Protocols : tidak ada data`); + } + + // 4. Top Negara + const countries = await netify.fetchTopCountries(1440, 15); + if (countries && Array.isArray(countries)) { + db.insertCountries(countries, fetchedAt); + console.log(`[Scheduler] OK Countries : ${countries.length} baris`); + } else { + console.log(`[Scheduler] -- Countries : tidak ada data`); + } + + // 5. Top Domain/DNS + const domains = await netify.fetchTopDomains(1440, 20); + if (domains && Array.isArray(domains)) { + db.insertDNS(domains, fetchedAt); + console.log(`[Scheduler] OK DNS : ${domains.length} baris`); + } else { + console.log(`[Scheduler] -- DNS : tidak ada data`); + } + + // 6. Flows — pakai local_ip sebagai proxy + const flows = await netify.fetchFlows(200); + if (flows && Array.isArray(flows)) { + db.insertFlows(flows, fetchedAt); + console.log(`[Scheduler] OK Flows : ${flows.length} baris`); + } else { + console.log(`[Scheduler] -- Flows : tidak ada data`); + } + + // 7. Threats — dari Events Status + const threats = await netify.fetchCyberThreats(1440, 50); + if (threats && Array.isArray(threats)) { + db.insertThreats(threats, fetchedAt); + console.log(`[Scheduler] OK Threats : ${threats.length} baris`); + } else { + console.log(`[Scheduler] -- Threats : tidak ada data`); + } + + // 8. Events Log + const events = await netify.fetchEvents(50); + if (events && Array.isArray(events)) { + db.insertEvents(events, fetchedAt); + console.log(`[Scheduler] OK Events : ${events.length} baris`); + } else { + console.log(`[Scheduler] -- Events : tidak ada data`); + } + + // 10. App Categories + const appCats = await netify.fetchTopAppCategories(1440, 15); + if (appCats?.length) { db.insertAppCategories(appCats, fetchedAt); console.log(`[Scheduler] OK AppCats : ${appCats.length} baris`); } + else console.log(`[Scheduler] -- AppCats : tidak ada data`); + + // 11. Continents + const continents = await netify.fetchTopContinents(1440, 10); + if (continents?.length) { db.insertContinents(continents, fetchedAt); console.log(`[Scheduler] OK Continents : ${continents.length} baris`); } + else console.log(`[Scheduler] -- Continents : tidak ada data`); + + // 12. Regions + const regions = await netify.fetchTopRegions(1440, 20); + if (regions?.length) { db.insertRegions(regions, fetchedAt); console.log(`[Scheduler] OK Regions : ${regions.length} baris`); } + else console.log(`[Scheduler] -- Regions : tidak ada data`); + + // 13. Cities + const cities = await netify.fetchTopCities(1440, 20); + if (cities?.length) { db.insertCities(cities, fetchedAt); console.log(`[Scheduler] OK Cities : ${cities.length} baris`); } + else console.log(`[Scheduler] -- Cities : tidak ada data`); + + // 14. VLANs + const vlans = await netify.fetchTopVLANs(1440, 20); + if (vlans?.length) { db.insertVLANs(vlans, fetchedAt); console.log(`[Scheduler] OK VLANs : ${vlans.length} baris`); } + else console.log(`[Scheduler] -- VLANs : tidak ada data`); + + // 15. Interfaces + const ifaces = await netify.fetchTopInterfaces(1440, 20); + if (ifaces?.length) { db.insertInterfaces(ifaces, fetchedAt); console.log(`[Scheduler] OK Interfaces : ${ifaces.length} baris`); } + else console.log(`[Scheduler] -- Interfaces : tidak ada data`); + + // 16. Flow Types + const flowTypes = await netify.fetchTopFlowTypes(1440, 10); + if (flowTypes?.length) { db.insertFlowTypes(flowTypes, fetchedAt); console.log(`[Scheduler] OK FlowTypes : ${flowTypes.length} baris`); } + else console.log(`[Scheduler] -- FlowTypes : tidak ada data`); + + // 17. Flow Origins + const flowOrigins = await netify.fetchTopFlowOrigins(1440, 10); + if (flowOrigins?.length) { db.insertFlowOrigins(flowOrigins, fetchedAt); console.log(`[Scheduler] OK FlowOrigin : ${flowOrigins.length} baris`); } + else console.log(`[Scheduler] -- FlowOrigin : tidak ada data`); + + // 18. IP Versions + const ipVersions = await netify.fetchTopIPVersions(1440, 5); + if (ipVersions?.length) { db.insertIPVersions(ipVersions, fetchedAt); console.log(`[Scheduler] OK IPVersions : ${ipVersions.length} baris`); } + else console.log(`[Scheduler] -- IPVersions : tidak ada data`); + + // 19. Remote IPs + const remoteIPs = await netify.fetchTopRemoteIPs(1440, 20); + if (remoteIPs?.length) { db.insertRemoteIPs(remoteIPs, fetchedAt); console.log(`[Scheduler] OK RemoteIPs : ${remoteIPs.length} baris`); } + else console.log(`[Scheduler] -- RemoteIPs : tidak ada data`); + + // 20. MAC Bandwidth + const macBW = await netify.fetchTopLocalMACs(1440, 50); + if (macBW?.length) { db.insertMACBandwidth(macBW, fetchedAt); console.log(`[Scheduler] OK MACBandwdh : ${macBW.length} baris`); } + else console.log(`[Scheduler] -- MACBandwdh : tidak ada data`); + + // 9. Bandwidth Timeline + const summary = await netify.fetchBandwidthSummary(1440); + const devCount = devices?.length ?? 0; + db.insertBandwidthTimeline({ ...summary, devices: devCount }, fetchedAt); + console.log(`[Scheduler] OK Timeline : saved`); + + // 21. TLS Versions + const tlsVer = await netify.fetchTLSVersions(1440, 10); + if (tlsVer?.length) { db.insertTLSVersions(tlsVer, fetchedAt); console.log(`[Scheduler] OK TLS Ver : ${tlsVer.length} baris`); } + else console.log(`[Scheduler] -- TLS Ver : tidak ada data`); + + // 22. TLS Ciphers + const tlsCipher = await netify.fetchTLSCiphers(1440, 15); + if (tlsCipher?.length) { db.insertTLSCiphers(tlsCipher, fetchedAt); console.log(`[Scheduler] OK TLS Cipher : ${tlsCipher.length} baris`); } + else console.log(`[Scheduler] -- TLS Cipher : tidak ada data`); + + // 23. TLS Security + const tlsSec = await netify.fetchTLSSecurity(1440, 10); + if (tlsSec?.length) { db.insertTLSSecurity(tlsSec, fetchedAt); console.log(`[Scheduler] OK TLS Sec : ${tlsSec.length} baris`); } + else console.log(`[Scheduler] -- TLS Sec : tidak ada data`); + + // 24. NetBIOS Hostnames + const netbios = await netify.fetchNetBIOSHostnames(1440, 30); + if (netbios?.length) { db.insertNetBIOSHostnames(netbios, fetchedAt); console.log(`[Scheduler] OK NetBIOS : ${netbios.length} baris`); } + else console.log(`[Scheduler] -- NetBIOS : tidak ada data`); + + // 25. Discovery OS (standalone — OS yang terdeteksi di jaringan) + const discOs = await netify.fetchTopDiscoveryOS(1440, 20); + if (discOs?.length) { db.insertDiscoveryOS(discOs, fetchedAt); console.log(`[Scheduler] OK DiscOS : ${discOs.length} baris`); } + else console.log(`[Scheduler] -- DiscOS : tidak ada data`); + + // 26. DHCP Class Fingerprint + const dhcpFp = await netify.fetchDHCPClassFingerprints(1440, 30); + if (dhcpFp?.length) { db.insertDHCPFingerprints(dhcpFp, fetchedAt); console.log(`[Scheduler] OK DHCP FP : ${dhcpFp.length} baris`); } + else console.log(`[Scheduler] -- DHCP FP : tidak ada data`); + + // 27. HTTP User-Agent + const userAgents = await netify.fetchHTTPUserAgents(1440, 30); + if (userAgents?.length) { db.insertHTTPUserAgents(userAgents, fetchedAt); console.log(`[Scheduler] OK UserAgent : ${userAgents.length} baris`); } + else console.log(`[Scheduler] -- UserAgent : tidak ada data`); + + // 28. HTTPS SNI Hostname + const sniHosts = await netify.fetchSNIHostnames(1440, 30); + if (sniHosts?.length) { db.insertSNIHostnames(sniHosts, fetchedAt); console.log(`[Scheduler] OK SNI Host : ${sniHosts.length} baris`); } + else console.log(`[Scheduler] -- SNI Host : tidak ada data`); + + // 29. SSL Server Common Name + const sslCN = await netify.fetchSSLServerCN(1440, 30); + if (sslCN?.length) { db.insertSSLServerCN(sslCN, fetchedAt); console.log(`[Scheduler] OK SSL CN : ${sslCN.length} baris`); } + else console.log(`[Scheduler] -- SSL CN : tidak ada data`); + + // 30. QUIC Hostname + const quicHosts = await netify.fetchQUICHostnames(1440, 30); + if (quicHosts?.length) { db.insertQUICHostnames(quicHosts, fetchedAt); console.log(`[Scheduler] OK QUIC Host : ${quicHosts.length} baris`); } + else console.log(`[Scheduler] -- QUIC Host : tidak ada data`); + + // 31. BitTorrent Info Hash + const btHashes = await netify.fetchBitTorrentInfoHashes(1440, 30); + if (btHashes?.length) { db.insertBitTorrentHashes(btHashes, fetchedAt); console.log(`[Scheduler] OK BT Hash : ${btHashes.length} baris`); } + else console.log(`[Scheduler] -- BT Hash : tidak ada data`); + + // 32. SSH Client (field: ssh_client) + const sshClient = await netify.fetchSSHClients(1440, 20); + if (sshClient?.length) { db.insertSSHVersions(sshClient, fetchedAt); console.log(`[Scheduler] OK SSH Client : ${sshClient.length} baris`); } + else console.log(`[Scheduler] -- SSH Client : tidak ada data`); + + // 32b. SSH Server (field: ssh_server) + const sshServer = await netify.fetchSSHServers(1440, 20); + if (sshServer?.length) { db.insertSSHVersions(sshServer, fetchedAt); console.log(`[Scheduler] OK SSH Server : ${sshServer.length} baris`); } + else console.log(`[Scheduler] -- SSH Server : tidak ada data`); + + // 33. mDNS Hostname (Chromecast, Apple TV, etc.) + const mdnsHosts = await netify.fetchMDNSHostnames(1440, 30); + if (mdnsHosts?.length) { db.insertMDNSHostnames(mdnsHosts, fetchedAt); console.log(`[Scheduler] OK mDNS Host : ${mdnsHosts.length} baris`); } + else console.log(`[Scheduler] -- mDNS Host : tidak ada data`); + + // ─── INTELLIGENCE 22-30 (derive dari data yang tersedia) ───────────────── + + // 34. Cryptocurrency Mining (derive dari apps + flows ke port mining) + const cryptoMining = await netify.fetchCryptoMining(50); + if (cryptoMining?.length) { db.insertCryptoMining(cryptoMining, fetchedAt); console.log(`[Scheduler] OK CryptoMine : ${cryptoMining.length} baris`); } + else console.log(`[Scheduler] -- CryptoMine : tidak ada data`); + + // 35. Device Discovery (derive dari flows + bandwidth per-IP) + const devDisc = await netify.fetchDeviceDiscovery(100); + if (devDisc?.length) { db.insertDeviceDiscovery(devDisc, fetchedAt); console.log(`[Scheduler] OK DevDisc : ${devDisc.length} baris`); } + else console.log(`[Scheduler] -- DevDisc : tidak ada data`); + + // 36. Encryption Audit (derive dari flows per-IP: port encrypted vs plain) + const encAudit = await netify.fetchEncryptionAudit(50); + if (encAudit?.length) { db.insertEncryptionAudit(encAudit, fetchedAt); console.log(`[Scheduler] OK EncAudit : ${encAudit.length} baris`); } + else console.log(`[Scheduler] -- EncAudit : tidak ada data`); + + // 37. Insecure Protocols (derive dari top protocols) + const insecProto = await netify.fetchInsecureProtocols(1440, 50); + if (insecProto?.length) { db.insertInsecureProtocols(insecProto, fetchedAt); console.log(`[Scheduler] OK InsecProto : ${insecProto.length} baris`); } + else console.log(`[Scheduler] -- InsecProto : tidak ada data`); + + // 38. IP Reputation (derive dari top remote_ip + high-risk countries) + const ipRep = await netify.fetchIPReputation(50); + if (ipRep?.length) { db.insertIPReputation(ipRep, fetchedAt); console.log(`[Scheduler] OK IPRepute : ${ipRep.length} baris`); } + else console.log(`[Scheduler] -- IPRepute : tidak ada data`); + + // 39. Server Discovery (derive dari flows ke port server well-known) + const srvDisc = await netify.fetchServerDiscovery(100); + if (srvDisc?.length) { db.insertServerDiscovery(srvDisc, fetchedAt); console.log(`[Scheduler] OK SrvDisc : ${srvDisc.length} baris`); } + else console.log(`[Scheduler] -- SrvDisc : tidak ada data`); + + // 40. Tor Detection (derive dari apps/hostnames mengandung "tor") + const torDet = await netify.fetchTorDetection(50); + if (torDet?.length) { db.insertTorDetection(torDet, fetchedAt); console.log(`[Scheduler] OK TorDet : ${torDet.length} baris`); } + else console.log(`[Scheduler] -- TorDet : tidak ada data`); + + // 41. Unencrypted Password (derive dari flows ke port cleartext auth) + const unencPwd = await netify.fetchUnencryptedPasswords(50); + if (unencPwd?.length) { db.insertUnencryptedPasswords(unencPwd, fetchedAt); console.log(`[Scheduler] OK UnencPwd : ${unencPwd.length} baris`); } + else console.log(`[Scheduler] -- UnencPwd : tidak ada data`); + + // 42. VPN Detection (derive dari apps/protocols/ports VPN) + const vpnDet = await netify.fetchVPNDetection(50); + if (vpnDet?.length) { db.insertVPNDetection(vpnDet, fetchedAt); console.log(`[Scheduler] OK VPNDet : ${vpnDet.length} baris`); } + else console.log(`[Scheduler] -- VPNDet : tidak ada data`); + + } catch (err) { + console.error('[Scheduler] ERROR:', err); + } finally { + isRunning = false; + console.log(`[Scheduler] Poll selesai.\n`); + } +} + +function startScheduler() { + runPoll(); + cron.schedule('* * * * *', () => runPoll()); + console.log('[Scheduler] Aktif. Polling setiap 1 menit.\n'); +} + +module.exports = { startScheduler, runPoll }; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js new file mode 100644 index 0000000..8b9a5d0 --- /dev/null +++ b/backend/server.js @@ -0,0 +1,32 @@ +// backend/server.js +const path = require('path'); +require('dotenv').config({ path: path.join(__dirname, '..', '.env.local') }); +const express = require('express'); +const cors = require('cors'); + +const app = express(); +const PORT = process.env.BACKEND_PORT || 3001; + +// ── Middleware ──────────────────────────────────────────────────────────────── +app.use(cors()); +app.use(express.json()); + +// ── API Routes ──────────────────────────────────────────────────────────────── +const dashboardRoutes = require('./routes/dashboard'); +app.use('/api/dashboard', dashboardRoutes); + +// ── Health Check ────────────────────────────────────────────────────────────── +app.get('/api/health', (req, res) => { + res.json({ ok: true, message: 'Netify Dashboard Backend berjalan!', time: new Date().toISOString() }); +}); + + +// ── Start Server ────────────────────────────────────────────────────────────── +app.listen(PORT, () => { + console.log(`\n🚀 API Server berjalan di http://localhost:${PORT}`); + console.log(`🔌 API Health : http://localhost:${PORT}/api/health\n`); + + // Mulai scheduler polling + const { startScheduler } = require('./scheduler'); + startScheduler(); +}); \ No newline at end of file diff --git a/backend/tests/netify.test.js b/backend/tests/netify.test.js new file mode 100644 index 0000000..68be824 --- /dev/null +++ b/backend/tests/netify.test.js @@ -0,0 +1,125 @@ +const test = require('node:test'); +const assert = require('node:assert'); +const axios = require('axios'); + +// Save the original axios get method +const originalGet = axios.get; + +// Helper to mock axios.get response +function mockAxiosGet(mockFn) { + axios.get = mockFn; +} + +// Restore original axios get +function restoreAxiosGet() { + axios.get = originalGet; +} + +test.afterEach(() => { + restoreAxiosGet(); +}); + +test('fetchEvents parses raw events successfully', async () => { + const netify = require('../netify'); + + // Mock API response + mockAxiosGet(async (url, config) => { + if (url.endsWith('/event/events')) { + return { + data: { + status_code: 0, + status_message: 'Success.', + data: [ + { + id: 254, + basename: 'new.device', + severity: 10, + label: 'New device detected', + description: JSON.stringify({ + default: 'New device {{ device_label }} discovered', + tags: { + device_label: 'Linux Device', + device_ip: '192.168.1.100' + } + }), + created_at: { + date: '2026-06-29 10:46:20' + } + } + ] + } + }; + } + throw new Error(`Unexpected mock URL: ${url}`); + }); + + const parsed = await netify.fetchEvents(50); + assert.ok(Array.isArray(parsed)); + assert.strictEqual(parsed.length, 1); + + const event = parsed[0]; + assert.strictEqual(event.event_id, 254); + assert.strictEqual(event.event_type, 'new.device'); + assert.strictEqual(event.severity, 'Warning'); + assert.strictEqual(event.description, 'New device Linux Device discovered'); + assert.strictEqual(event.ip_address, '192.168.1.100'); + assert.strictEqual(event.event_at, '2026-06-29 10:46:20'); +}); + +test('fetchDiscoveredDevices merges devices and flows successfully', async () => { + const netify = require('../netify'); + + mockAxiosGet(async (url, config) => { + if (url.endsWith('/intelligence/discovery/devices')) { + return { + data: { + status_code: 0, + data: [ + { + ip: { address: '10.250.192.226' }, + mac_address: 'b2:87:41:96:a7:7b', + mac_vendor: 'Local', + discovery_mac: { + address: 'b2:87:41:96:a7:7b', + discovery_hardware: 'Linux Device' + }, + discovery_type: { label: 'Computer' }, + discovery_os: { label: 'Linux' } + } + ] + } + }; + } + if (url.endsWith('/data/stats/top/local_ip/download')) { + return { + data: { + status_code: 0, + data: [ + { local_ip: { address: '10.250.192.226' }, download: 1000000 } + ] + } + }; + } + if (url.endsWith('/data/stats/top/local_ip/upload')) { + return { + data: { + status_code: 0, + data: [ + { local_ip: { address: '10.250.192.226' }, upload: 500000 } + ] + } + }; + } + throw new Error(`Unexpected mock URL: ${url}`); + }); + + const devices = await netify.fetchDiscoveredDevices(1440, 50); + assert.ok(Array.isArray(devices)); + assert.strictEqual(devices.length, 1); + + const dev = devices[0]; + assert.strictEqual(dev.ip_address, '10.250.192.226'); + assert.strictEqual(dev.mac_address, 'b2:87:41:96:a7:7b'); + assert.strictEqual(dev.download, 1000000); + assert.strictEqual(dev.upload, 500000); +}); diff --git a/next.config.ts b/next.config.ts index e9ffa30..9708400 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,14 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + async rewrites() { + return [ + { + source: '/api/:path*', + destination: `${process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:3001'}/api/:path*`, + }, + ]; + }, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 77a7cd2..c825d16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,15 +8,31 @@ "name": "netify-app", "version": "0.1.0", "dependencies": { + "axios": "^1.18.1", + "better-sqlite3": "^12.11.1", + "clsx": "^2.1.1", + "concurrently": "^10.0.3", + "cors": "^2.8.6", + "d3-scale": "^4.0.2", + "dotenv": "^17.4.2", + "express": "^5.2.1", + "lucide-react": "^1.21.0", "next": "16.2.9", + "node-cron": "^4.5.0", "react": "19.2.4", - "react-dom": "19.2.4" + "react-dom": "19.2.4", + "react-simple-maps": "^3.0.0", + "recharts": "^3.9.0", + "swr": "^2.4.2", + "tailwind-merge": "^3.6.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/d3-scale": "^4.0.9", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@types/react-simple-maps": "^3.0.6", "eslint": "^9", "eslint-config-next": "16.2.9", "tailwindcss": "^4", @@ -1306,6 +1322,42 @@ "node": ">=12.4.0" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.8", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.8.tgz", + "integrity": "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1313,6 +1365,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1616,6 +1680,114 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-2.0.7.tgz", + "integrity": "sha512-RIXlxPdxvX+LAZFv+t78CuYpxYag4zuw9mZc+AwfB8tZpKU90rMEn2il2ADncmeZlb7nER9dDsJpRisA3lRvjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-selection": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-2.0.5.tgz", + "integrity": "sha512-71BorcY0yXl12S7lvb01JdaN9TpeUHBDb4RRhSq8U8BEkX/nIk5p7Byho+ZRTsx5nYLMpAbY3qt5EhqFzfGJlw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-zoom": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-2.0.7.tgz", + "integrity": "sha512-JWke4E8ZyrKUQ68ESTWSK16fVb0OYnaiJ+WXJRYxKLn4aXU0o4CLYxMWBEiouUfO3TTCoyroOrGPcBG6u1aAxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "^2", + "@types/d3-selection": "^2" + } + }, + "node_modules/@types/d3-zoom/node_modules/@types/d3-color": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-2.0.6.tgz", + "integrity": "sha512-tbaFGDmJWHqnenvk3QGSvD3RVwr631BjKRD7Sc7VLRgrdX5mk5hTyoeBL6rXZaeoXzmZwIl1D2HPogEdt1rHBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-zoom/node_modules/@types/d3-interpolate": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-2.0.5.tgz", + "integrity": "sha512-UINE41RDaUMbulp+bxQMDnhOi51rh5lA2dG+dWZU0UY/IwQiG/u2x8TfnWYU9+xwGdXsJoAvrBYUEQl0r91atg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-color": "^2" + } + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -1623,6 +1795,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1667,6 +1846,25 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/react-simple-maps": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/react-simple-maps/-/react-simple-maps-3.0.6.tgz", + "integrity": "sha512-hR01RXt6VvsE41FxDd+Bqm1PPGdKbYjCYVtCgh38YeBPt46z3SwmWPWu2L3EdCAP6bd6VYEgztucihRw1C0Klg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-geo": "^2", + "@types/d3-zoom": "^2", + "@types/geojson": "*", + "@types/react": "*" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.0.tgz", @@ -2316,6 +2514,19 @@ "win32" ] }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.17.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", @@ -2339,6 +2550,18 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", @@ -2356,6 +2579,18 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -2566,6 +2801,12 @@ "node": ">= 0.4" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -2592,6 +2833,18 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -2609,6 +2862,26 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.10.40", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz", @@ -2621,6 +2894,77 @@ "node": ">=6.0.0" } }, + "node_modules/better-sqlite3": { + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", + "integrity": "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/brace-expansion": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", @@ -2679,6 +3023,39 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -2702,7 +3079,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -2716,7 +3092,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -2776,12 +3151,41 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2802,6 +3206,24 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2809,6 +3231,76 @@ "dev": true, "license": "MIT" }, + "node_modules/concurrently": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.3.tgz", + "integrity": "sha512-hc3LH4UaKWd/bbyDK/IGVa4RB6PtQ3CUYwtrkzqHn+wIG3Hr5fhpRlk0L/gCa8ZE1L/Ufj50Zho69cI5w8SQBA==", + "license": "MIT", + "dependencies": { + "chalk": "5.6.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.4", + "supports-color": "10.2.2", + "tree-kill": "1.2.2", + "yargs": "18.0.0" + }, + "bin": { + "conc": "dist/bin/index.js", + "concurrently": "dist/bin/index.js" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2816,6 +3308,41 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2838,6 +3365,244 @@ "dev": true, "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-2.0.0.tgz", + "integrity": "sha512-S/m2VsXI7gAti2pBoLClFFTMOO1HTtT0j99AuXLoGFKO6deHDdnv6ZGTxSTTUTgO1zVcv82fCOtDjYK4EECmWA==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-drag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-2.0.0.tgz", + "integrity": "sha512-g9y9WbMnF5uqB9qKqwIIa/921RYWzlUDv9Jl1/yONQwxbOfszAWTCm8u7HOTgJgRDXiRZN56cHT9pd24dmXs8w==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-dispatch": "1 - 2", + "d3-selection": "2" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-2.0.2.tgz", + "integrity": "sha512-8pM1WGMLGFuhq9S+FpPURxic+gKzjluCD/CHTuUF3mXMeiCo0i6R0tO1s4+GArRFde96SLcW/kOFRjoAosPsFA==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^2.5.0" + } + }, + "node_modules/d3-geo/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-geo/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-2.0.0.tgz", + "integrity": "sha512-XoGGqhLUN/W14NmaqcO/bb1nqjDAw5WtSYb2X8wiuQWvSZUsUVYsOSkOybUrNvcBjaywBdYPy03eXHMXjk9nZA==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-2.0.0.tgz", + "integrity": "sha512-42ltAGgJesfQE3u9LuuBHNbGrI/AJjNL2OAUdclE70UE6Vy239GCBEYD38uBPoLeNsOhFStGpPI0BAOV+HMxog==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-color": "1 - 2", + "d3-dispatch": "1 - 2", + "d3-ease": "1 - 2", + "d3-interpolate": "1 - 2", + "d3-timer": "1 - 2" + }, + "peerDependencies": { + "d3-selection": "2" + } + }, + "node_modules/d3-transition/node_modules/d3-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz", + "integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-transition/node_modules/d3-ease": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-2.0.0.tgz", + "integrity": "sha512-68/n9JWarxXkOWMshcT5IcjbB+agblQUaIsbnXmrzejn2O82n3p2A9R2zEB9HIEFWKFwPAEDDN8gR0VdSAyyAQ==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-transition/node_modules/d3-interpolate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-2.0.1.tgz", + "integrity": "sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-color": "1 - 2" + } + }, + "node_modules/d3-transition/node_modules/d3-timer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-2.0.0.tgz", + "integrity": "sha512-TO4VLh0/420Y/9dO3+f9abDEFYeCUr2WZRlxJvbp4HPTQcSylXNiL6yZa9FIUvV1yRiFufl1bszTCLDqv9PWNA==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-zoom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-2.0.0.tgz", + "integrity": "sha512-fFg7aoaEm9/jf+qfstak0IYpnesZLiMX6GZvXtUSdv8RH2o4E2qeelgdU09eKS6wGuiGMfcnMI0nTIqWzRHGpw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-dispatch": "1 - 2", + "d3-drag": "2", + "d3-interpolate": "1 - 2", + "d3-selection": "2", + "d3-transition": "2" + } + }, + "node_modules/d3-zoom/node_modules/d3-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz", + "integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-zoom/node_modules/d3-interpolate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-2.0.1.tgz", + "integrity": "sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-color": "1 - 2" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -2903,7 +3668,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2917,6 +3681,36 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2960,11 +3754,37 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -2983,11 +3803,22 @@ "node": ">=0.10.0" } }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -2998,6 +3829,12 @@ "node": ">= 0.4" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.380", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.380.tgz", @@ -3012,6 +3849,24 @@ "dev": true, "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.21.6", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", @@ -3118,7 +3973,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3128,7 +3982,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3166,7 +4019,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -3179,7 +4031,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -3225,16 +4076,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.49.0.tgz", + "integrity": "sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -3654,6 +4520,73 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3728,6 +4661,12 @@ "node": ">=16.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3741,6 +4680,27 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3779,6 +4739,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -3795,11 +4775,71 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3859,11 +4899,31 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -3888,7 +4948,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -3929,6 +4988,12 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3976,7 +5041,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4048,7 +5112,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4061,7 +5124,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -4077,7 +5139,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -4103,6 +5164,75 @@ "hermes-estree": "0.25.1" } }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4113,6 +5243,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -4140,6 +5280,18 @@ "node": ">=0.8.19" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -4155,6 +5307,24 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -4441,6 +5611,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -5084,6 +6260,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.21.0.tgz", + "integrity": "sha512-reEZMXq8Qdd5jg5XYkQ5TR1fB/GiQ7ih4vcrthYDtgjSDwh0i6/YLiGjsWsIwgN49gpAnd4J2elSNzncMEEUUQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -5098,12 +6283,32 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" } }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5128,6 +6333,43 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -5145,17 +6387,21 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -5176,6 +6422,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/napi-postinstall": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", @@ -5199,6 +6451,15 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/next": { "version": "16.2.9", "resolved": "https://registry.npmjs.org/next/-/next-16.2.9.tgz", @@ -5280,6 +6541,39 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-cron": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.5.0.tgz", + "integrity": "sha512-4Trh+kjvbXokyJkwQumvD5YAgeJfgHLR/sKyu71uSmxfCR5QMO1hldpvmFZOICN5pLgNY+J5Y8+ar3XKo5/4tQ==", + "license": "ISC", + "engines": { + "node": ">=20" + } + }, "node_modules/node-exports-info": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", @@ -5313,7 +6607,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5323,7 +6616,6 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5432,6 +6724,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5513,6 +6826,15 @@ "node": ">=6" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5540,6 +6862,16 @@ "dev": true, "license": "MIT" }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5598,6 +6930,33 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5620,6 +6979,38 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5630,6 +7021,22 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5651,6 +7058,58 @@ ], "license": "MIT" }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", @@ -5679,6 +7138,105 @@ "dev": true, "license": "MIT" }, + "node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-simple-maps": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/react-simple-maps/-/react-simple-maps-3.0.0.tgz", + "integrity": "sha512-vKNFrvpPG8Vyfdjnz5Ne1N56rZlDfHXv5THNXOVZMqbX1rWZA48zQuYT03mx6PAKanqarJu/PDLgshIZAfHHqw==", + "license": "MIT", + "dependencies": { + "d3-geo": "^2.0.2", + "d3-selection": "^2.0.0", + "d3-zoom": "^2.0.0", + "topojson-client": "^3.1.0" + }, + "peerDependencies": { + "prop-types": "^15.7.2", + "react": "^16.8.0 || 17.x || 18.x", + "react-dom": "^16.8.0 || 17.x || 18.x" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/recharts": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.9.0.tgz", + "integrity": "sha512-dCEcE9y20c8H2tkVeByrAXhhnBJk6/QLbxKmn+dJUptOfc5NMjwRh1jo0vZPRLD+5dMrHrP+hPEsfbGBMfnf5Q==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.2.0", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5723,6 +7281,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/reselect": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", + "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", + "license": "MIT" + }, "node_modules/resolve": { "version": "2.0.0-next.7", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", @@ -5778,6 +7342,22 @@ "node": ">=0.10.0" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5802,6 +7382,15 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-array-concat": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", @@ -5822,6 +7411,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -5857,6 +7466,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -5873,6 +7488,51 @@ "semver": "bin/semver.js" } }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -5922,6 +7582,12 @@ "node": ">= 0.4" } }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/sharp": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", @@ -6003,11 +7669,22 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", + "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/side-channel": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -6027,7 +7704,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -6044,7 +7720,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -6063,7 +7738,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -6079,6 +7753,51 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -6095,6 +7814,15 @@ "dev": true, "license": "MIT" }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -6109,6 +7837,38 @@ "node": ">= 0.4" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -6223,6 +7983,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -6295,6 +8070,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swr": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.4.2.tgz", + "integrity": "sha512-ej644Y2bvkIajfR32KGeSSdBXQW+ScjGjkybZgSE7kFpk9eGnV44XY9FJylXi+W75pavSX1PVNB57W5EbhGIYw==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/tailwind-merge": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", + "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz", @@ -6316,6 +8114,40 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -6377,6 +8209,38 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/topojson-client": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz", + "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==", + "license": "ISC", + "dependencies": { + "commander": "2" + }, + "bin": { + "topo2geo": "bin/topo2geo", + "topomerge": "bin/topomerge", + "topoquantize": "bin/topoquantize" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -6422,6 +8286,18 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6435,6 +8311,37 @@ "node": ">= 0.8.0" } }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -6577,6 +8484,15 @@ "dev": true, "license": "MIT" }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/unrs-resolver": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", @@ -6656,6 +8572,52 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6771,6 +8733,50 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -6778,6 +8784,32 @@ "dev": true, "license": "ISC" }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 9b858ed..d46017b 100644 --- a/package.json +++ b/package.json @@ -3,21 +3,38 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "concurrently \"next dev\" \"node backend/server.js\"", "build": "next build", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "backend": "node backend/server.js" }, "dependencies": { + "axios": "^1.18.1", + "better-sqlite3": "^12.11.1", + "clsx": "^2.1.1", + "concurrently": "^10.0.3", + "cors": "^2.8.6", + "d3-scale": "^4.0.2", + "dotenv": "^17.4.2", + "express": "^5.2.1", + "lucide-react": "^1.21.0", "next": "16.2.9", + "node-cron": "^4.5.0", "react": "19.2.4", - "react-dom": "19.2.4" + "react-dom": "19.2.4", + "react-simple-maps": "^3.0.0", + "recharts": "^3.9.0", + "swr": "^2.4.2", + "tailwind-merge": "^3.6.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/d3-scale": "^4.0.9", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@types/react-simple-maps": "^3.0.6", "eslint": "^9", "eslint-config-next": "16.2.9", "tailwindcss": "^4", diff --git a/scratch_fix.js b/scratch_fix.js new file mode 100644 index 0000000..3005224 --- /dev/null +++ b/scratch_fix.js @@ -0,0 +1,14 @@ +const fs = require('fs'); +const files = [ + 'src/app/network-infrastructure/page.tsx', + 'src/app/dpi-analytics/page.tsx', + 'src/app/intelligence/page.tsx' +]; + +files.forEach(f => { + let text = fs.readFileSync(f, 'utf8'); + text = text.replace(/accessorKey:\s*"([^"]+)"/g, 'accessor: (row: any) => row.$1'); + text = text.replace(/accessorKey:\s*\(/g, 'accessor: ('); + fs.writeFileSync(f, text); +}); +console.log('Fixed accessorKey'); diff --git a/scratch_test.js b/scratch_test.js new file mode 100644 index 0000000..593b248 --- /dev/null +++ b/scratch_test.js @@ -0,0 +1,35 @@ +const fs = require('fs'); +const fetch = global.fetch; + +const env = fs.readFileSync('.env.local', 'utf8').split('\n').reduce((acc, line) => { + const match = line.match(/^([^#\s=]+)=(.*)$/); + if (match) { + acc[match[1]] = match[2]; + } + return acc; +}, {}); + +async function test() { + try { + const API_URL = env.NETIFY_BASE_URL ? env.NETIFY_BASE_URL + '/assets/agents' : 'https://manager.netify.ai/api/v1/assets/agents'; + const headers = { 'Content-Type': 'application/json' }; + + if (env.NETIFY_JWT_TOKEN && env.NETIFY_SITE_UUID) { + headers['x-api-key'] = env.NETIFY_JWT_TOKEN; + headers['x-net-site'] = env.NETIFY_SITE_UUID; + } else if (env.NETIFY_API_KEY) { + headers['x-api-key'] = env.NETIFY_API_KEY; + } + + console.log('Fetching', API_URL); + console.log('Headers:', Object.keys(headers)); + + const res = await fetch(API_URL, { headers }); + console.log('Status:', res.status, res.statusText); + const data = await res.text(); + console.log('Data:', data.substring(0, 500)); + } catch(e) { + console.error(e); + } +} +test(); diff --git a/src/app/agents/page.tsx b/src/app/agents/page.tsx new file mode 100644 index 0000000..dca7eed --- /dev/null +++ b/src/app/agents/page.tsx @@ -0,0 +1,250 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { getAgents, createAgent, updateAgent, deleteAgent, Agent } from "@/lib/actions/agents"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card"; +import { DataTable, Column } from "@/components/ui/DataTable"; +import { Modal } from "@/components/ui/Modal"; +import { Badge } from "@/components/ui/Badge"; +import { Loader2, Plus, Edit, Trash2, Server, CheckCircle2, XCircle } from "lucide-react"; + +export default function AgentsPage() { + const [agents, setAgents] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(""); + + // Modals state + const [isCreateOpen, setIsCreateOpen] = useState(false); + const [isEditOpen, setIsEditOpen] = useState(false); + const [isDeleteOpen, setIsDeleteOpen] = useState(false); + + // Form state + const [selectedAgent, setSelectedAgent] = useState(null); + const [formData, setFormData] = useState({ agentId: "", label: "" }); + const [isSubmitting, setIsSubmitting] = useState(false); + + const loadAgents = async () => { + setIsLoading(true); + try { + const data = await getAgents(); + setAgents(data); + } catch (e: any) { + setError(e.message); + } + setIsLoading(false); + }; + + useEffect(() => { + loadAgents(); + }, []); + + const openCreate = () => { + setFormData({ agentId: "", label: "" }); + setIsCreateOpen(true); + }; + + const openEdit = (agent: Agent) => { + setSelectedAgent(agent); + setFormData({ agentId: agent.uuid || agent.serial, label: agent.label || "" }); + setIsEditOpen(true); + }; + + const openDelete = (agent: Agent) => { + setSelectedAgent(agent); + setIsDeleteOpen(true); + }; + + const handleCreate = async (e: React.FormEvent) => { + e.preventDefault(); + setIsSubmitting(true); + const res = await createAgent(formData.agentId, formData.label); + setIsSubmitting(false); + + if (res.success) { + setIsCreateOpen(false); + loadAgents(); + } else { + alert(res.message); + } + }; + + const handleUpdate = async (e: React.FormEvent) => { + e.preventDefault(); + if (!selectedAgent) return; + + setIsSubmitting(true); + const res = await updateAgent(selectedAgent.uuid || selectedAgent.serial, formData.label); + setIsSubmitting(false); + + if (res.success) { + setIsEditOpen(false); + loadAgents(); + } else { + alert(res.message); + } + }; + + const handleDelete = async () => { + if (!selectedAgent) return; + + setIsSubmitting(true); + const res = await deleteAgent(selectedAgent.uuid || selectedAgent.serial); + setIsSubmitting(false); + + if (res.success) { + setIsDeleteOpen(false); + loadAgents(); + } else { + alert(res.message); + } + }; + + const columns: Column[] = [ + { header: "UUID / Serial", accessor: (row) => {row.uuid || row.serial} }, + { header: "Label", accessor: (row) => row.label || No label }, + { header: "Provisioned", accessor: (row) => row.provisioned ? Yes : No }, + { header: "Activated", accessor: (row) => row.activated ? Yes : No }, + { header: "Last Seen", accessor: (row) => {row.last_seen_at?.human || '-'} }, + { + header: "Actions", + accessor: (row) => ( +
+ + +
+ ) + }, + ]; + + return ( +
+
+

+ + Agents Inventory + {isLoading && } +

+ +
+ + + + Active Agents {agents.length > 0 ? `(${agents.length})` : ''} + + + {error ? ( +
+ {error} +
+ ) : ( + { + const q = query.toLowerCase(); + return (row.uuid || "").toLowerCase().includes(q) || + (row.serial || "").toLowerCase().includes(q) || + (row.label || "").toLowerCase().includes(q); + }} + /> + )} +
+
+ + {/* Create Modal */} + setIsCreateOpen(false)} title="Provision New Agent"> +
+
+ + setFormData({...formData, agentId: e.target.value})} + className="w-full bg-slate-900 border border-slate-700 rounded-md px-3 py-2 text-sm text-white focus:outline-none focus:ring-2 focus:ring-primary/50" + /> +
+
+ + setFormData({...formData, label: e.target.value})} + className="w-full bg-slate-900 border border-slate-700 rounded-md px-3 py-2 text-sm text-white focus:outline-none focus:ring-2 focus:ring-primary/50" + /> +
+
+ + +
+
+
+ + {/* Edit Modal */} + setIsEditOpen(false)} title="Edit Agent"> +
+
+ + +
+
+ + setFormData({...formData, label: e.target.value})} + className="w-full bg-slate-900 border border-slate-700 rounded-md px-3 py-2 text-sm text-white focus:outline-none focus:ring-2 focus:ring-primary/50" + /> +
+
+ + +
+
+
+ + {/* Delete Modal */} + setIsDeleteOpen(false)} title="Confirm Deletion"> +
+

+ Are you sure you want to delete the agent {selectedAgent?.label || selectedAgent?.uuid}? This action cannot be undone. +

+
+ + +
+
+
+ +
+ ); +} diff --git a/src/app/api/[...route]/route.ts b/src/app/api/[...route]/route.ts new file mode 100644 index 0000000..c12e0e4 --- /dev/null +++ b/src/app/api/[...route]/route.ts @@ -0,0 +1,70 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export async function GET(req: NextRequest, props: { params: Promise<{ route: string[] }> }) { + return handleProxy(req, props); +} + +export async function POST(req: NextRequest, props: { params: Promise<{ route: string[] }> }) { + return handleProxy(req, props); +} + +export async function PUT(req: NextRequest, props: { params: Promise<{ route: string[] }> }) { + return handleProxy(req, props); +} + +export async function DELETE(req: NextRequest, props: { params: Promise<{ route: string[] }> }) { + return handleProxy(req, props); +} + +export async function PATCH(req: NextRequest, props: { params: Promise<{ route: string[] }> }) { + return handleProxy(req, props); +} + +async function handleProxy(req: NextRequest, { params }: { params: Promise<{ route: string[] }> }) { + try { + const route = (await params).route.join('/'); + const baseUrl = process.env.NETIFY_BASE_URL || 'https://manager.netify.ai/api/v1'; + + // Construct the destination URL including search parameters + const url = new URL(req.url); + const destinationUrl = `${baseUrl}/${route}${url.search}`; + + // Prepare headers + const headers = new Headers(); + headers.set('Content-Type', 'application/json'); + + if (process.env.NETIFY_JWT_TOKEN && process.env.NETIFY_SITE_UUID) { + headers.set('x-api-key', process.env.NETIFY_JWT_TOKEN); + headers.set('x-net-site', process.env.NETIFY_SITE_UUID); + } else if (process.env.NETIFY_API_KEY) { + headers.set('x-api-key', process.env.NETIFY_API_KEY); + } + + // Some requests like GET cannot have a body + const hasBody = !['GET', 'HEAD'].includes(req.method); + let body = null; + + if (hasBody) { + body = await req.text(); + } + + const response = await fetch(destinationUrl, { + method: req.method, + headers, + body: body || undefined, + }); + + // Return raw text so we don't try to parse it if it's not JSON + const responseText = await response.text(); + + return new NextResponse(responseText, { + status: response.status, + headers: { + 'Content-Type': response.headers.get('content-type') || 'application/json', + } + }); + } catch (error: any) { + console.error('[Proxy Error]', error); + return NextResponse.json({ ok: false, message: error.message }, { status: 500 }); + } +} diff --git a/src/app/apps/page.tsx b/src/app/apps/page.tsx new file mode 100644 index 0000000..94200c5 --- /dev/null +++ b/src/app/apps/page.tsx @@ -0,0 +1,114 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useTopApps } from "@/lib/api"; +import { DataTable, Column } from "@/components/ui/DataTable"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card"; +import { Loader2 } from "lucide-react"; +import { fmtBytes } from "@/lib/utils"; +import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer } from "recharts"; + +export default function AppsPage() { + const [mounted, setMounted] = useState(false); + const { data, isLoading } = useTopApps(50); // fetch 50 apps + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) return null; + + const top5 = data.slice(0, 5); + + const columns: Column[] = [ + { header: "#", accessor: (row, i) => i + 1 }, + { + header: "Application", + accessor: (row) => ( +
+ {row.favicon && ( + {row.app_label} + )} + {row.app_label} +
+ ) + }, + { header: "Category", accessor: (row) => row.category || "-" }, + { + header: "Download", + accessor: (row) => {fmtBytes(row.download)} + }, + { + header: "Upload", + accessor: (row) => {fmtBytes(row.upload)} + }, + { + header: "Total", + accessor: (row) => {fmtBytes(row.download + row.upload)} + } + ]; + + return ( +
+
+

+ Applications + {isLoading && } +

+
+ + + + Top 5 Applications by Volume + + +
+ + + + + { + const gb = value / 1073741824; + const mb = value / 1048576; + return gb >= 1 ? `${gb.toFixed(1)}GB` : `${mb.toFixed(0)}MB`; + }} + /> + [fmtBytes(value as number), 'Volume']} + /> + + + + +
+
+
+ + + + All Applications ({data.length}) + + + { + const q = query.toLowerCase(); + return ( + (row.app_label || "").toLowerCase().includes(q) || + (row.category || "").toLowerCase().includes(q) + ); + }} + /> + + +
+ ); +} diff --git a/src/app/devices/page.tsx b/src/app/devices/page.tsx new file mode 100644 index 0000000..ca7c16e --- /dev/null +++ b/src/app/devices/page.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card"; +import { DataTable, Column } from "@/components/ui/DataTable"; +import { useState, useEffect } from "react"; +import { useDevices, DeviceStat } from "@/lib/api"; +import { fmtBytes } from "@/lib/utils"; +import { Loader2 } from "lucide-react"; + +export default function DevicesPage() { + const [mounted, setMounted] = useState(false); + const devices = useDevices(100); + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) return null; + + const columns: Column[] = [ + { header: "#", accessor: (row, i) => i + 1 }, + { header: "IP Address", accessor: (row) => {row.ip_address || '-'} }, + { header: "MAC Address", accessor: (row) => row.mac_address ? {row.mac_address} : '-' }, + { header: "Type", accessor: (row) => row.device_type || '-' }, + { header: "OS", accessor: (row) => row.os_label || '-' }, + { header: "Manufacturer", accessor: (row) => row.manufacturer || '-' }, + { header: "Download", accessor: (row) => {fmtBytes(row.download)}, className: "text-right" }, + { header: "Upload", accessor: (row) => {fmtBytes(row.upload)}, className: "text-right" }, + ]; + + return ( +
+
+
+

+ Devices + {devices.isLoading && } +

+

Monitor and manage connected network devices.

+
+
+ + + + Active Devices {devices.data ? `(${devices.data.length})` : ''} + + + { + const q = query.toLowerCase(); + return (row.ip_address || "").toLowerCase().includes(q) || + (row.mac_address || "").toLowerCase().includes(q) || + (row.os_label || "").toLowerCase().includes(q) || + (row.device_type || "").toLowerCase().includes(q); + }} + /> + + +
+ ); +} diff --git a/src/app/dns/page.tsx b/src/app/dns/page.tsx new file mode 100644 index 0000000..05a7a48 --- /dev/null +++ b/src/app/dns/page.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useDNS } from "@/lib/api"; +import { DataTable, Column } from "@/components/ui/DataTable"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card"; +import { Loader2 } from "lucide-react"; + +export default function DNSPage() { + const [mounted, setMounted] = useState(false); + const { data, isLoading } = useDNS(100); // fetch 100 DNS queries + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) return null; + + const columns: Column[] = [ + { header: "#", accessor: (row, i) => i + 1 }, + { + header: "Domain", + accessor: (row) => {row.domain} + }, + { header: "Application", accessor: (row) => row.app_label || "-" }, + { header: "Category", accessor: (row) => row.category || "-" }, + { + header: "Query Count", + accessor: (row) => {row.query_count.toLocaleString()} + } + ]; + + return ( +
+
+

+ DNS Intelligence + {isLoading && } +

+
+ + + + Top DNS Queries ({data.length}) + + + { + const q = query.toLowerCase(); + return ( + (row.domain || "").toLowerCase().includes(q) || + (row.app_label || "").toLowerCase().includes(q) || + (row.category || "").toLowerCase().includes(q) + ); + }} + /> + + +
+ ); +} diff --git a/src/app/dpi-analytics/page.tsx b/src/app/dpi-analytics/page.tsx new file mode 100644 index 0000000..d1f5e37 --- /dev/null +++ b/src/app/dpi-analytics/page.tsx @@ -0,0 +1,217 @@ +"use client"; + +import { useState } from "react"; +import { Tabs, Tab } from "@/components/ui/Tabs"; +import { DataTable, Column } from "@/components/ui/DataTable"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card"; +import { + useNetbios, useDiscoveryOs, useDhcpFingerprints, + useHttpUserAgents, useSniHostnames, useSslServerCn, + useQuicHostnames, useBittorrentHashes, useSshVersions, + useMdnsHostnames +} from "@/lib/api-advanced"; + +export default function DpiAnalyticsPage() { + // Hostnames & Servers Tab + const { data: sniHostnames, isLoading: loadingSni } = useSniHostnames(20); + const { data: sslServerCn, isLoading: loadingSslCn } = useSslServerCn(20); + const { data: quicHostnames, isLoading: loadingQuic } = useQuicHostnames(20); + const { data: netbios, isLoading: loadingNetbios } = useNetbios(20); + const { data: mdnsHostnames, isLoading: loadingMdns } = useMdnsHostnames(20); + + // Client Fingerprints Tab + const { data: discoveryOs, isLoading: loadingOs } = useDiscoveryOs(20); + const { data: dhcpFingerprints, isLoading: loadingDhcp } = useDhcpFingerprints(20); + const { data: httpUserAgents, isLoading: loadingHttpUa } = useHttpUserAgents(20); + + // Specific Protocols Tab + const { data: sshVersions, isLoading: loadingSsh } = useSshVersions(20); + const { data: bittorrentHashes, isLoading: loadingBittorrent } = useBittorrentHashes(20); + + const formatBytes = (bytes: number) => { + if (bytes === 0 || !bytes) return "0 B"; + const k = 1024; + const sizes = ["B", "KB", "MB", "GB", "TB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; + }; + + return ( +
+
+

DPI Analytics

+

Deep Packet Inspection metadata and fingerprinting.

+
+ + + +
+ + + SNI Hostnames (TLS) + + + row.sni_hostname }, + { header: "Total", accessor: (row) => formatBytes(row.total) }, + ]} + /> + + + + + SSL Server Common Names + + + row.ssl_server_cn }, + { header: "Total", accessor: (row) => formatBytes(row.total) }, + ]} + /> + + + + + QUIC Hostnames + + + row.quic_hostname }, + { header: "Total", accessor: (row) => formatBytes(row.total) }, + ]} + /> + + + + + NetBIOS Hostnames + + + row.hostname }, + { header: "Total", accessor: (row) => formatBytes(row.total) }, + ]} + /> + + + + + mDNS Hostnames + + + row.mdns_hostname }, + { header: "Total", accessor: (row) => formatBytes(row.total) }, + ]} + /> + + +
+
+ + +
+ + + Operating Systems Discovery + + + row.os_label }, + { header: "Download", accessor: (row) => formatBytes(row.download) }, + { header: "Upload", accessor: (row) => formatBytes(row.upload) }, + { header: "Total", accessor: (row) => formatBytes(row.total) }, + ]} + /> + + + + + DHCP Fingerprints + + + row.fingerprint }, + { header: "Total", accessor: (row) => formatBytes(row.total) }, + ]} + /> + + + + + HTTP User Agents + + + row.user_agent }, + { header: "Total", accessor: (row) => formatBytes(row.total) }, + ]} + /> + + +
+
+ + +
+ + + SSH Versions + + + row.ssh_version }, + { header: "Total", accessor: (row) => formatBytes(row.total) }, + ]} + /> + + + + + BitTorrent Hashes + + + row.label }, + { header: "Info Hash", accessor: (row: any) => row.info_hash }, + { header: "Total", accessor: (row) => formatBytes(row.total) }, + ]} + /> + + +
+
+
+
+ ); +} diff --git a/src/app/events/page.tsx b/src/app/events/page.tsx new file mode 100644 index 0000000..e644fd8 --- /dev/null +++ b/src/app/events/page.tsx @@ -0,0 +1,93 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useEvents } from "@/lib/api"; +import { DataTable, Column } from "@/components/ui/DataTable"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card"; +import { Loader2, AlertCircle, Info, AlertTriangle } from "lucide-react"; + +export default function EventsPage() { + const [mounted, setMounted] = useState(false); + const { data, isLoading } = useEvents(100); // fetch 100 events + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) return null; + + const getSeverityIcon = (severity: string) => { + switch (severity.toLowerCase()) { + case 'critical': + case 'high': + return ; + case 'medium': + case 'warning': + return ; + default: + return ; + } + }; + + const columns: Column[] = [ + { header: "#", accessor: (row, i) => i + 1 }, + { + header: "Timestamp", + accessor: (row) => { + try { + return new Date(row.timestamp).toLocaleString(); + } catch { + return row.timestamp; + } + } + }, + { + header: "Severity", + accessor: (row) => ( +
+ {getSeverityIcon(row.severity)} + {row.severity} +
+ ) + }, + { header: "Event Type", accessor: (row) => row.event_type }, + { + header: "Source IP", + accessor: (row) => {row.source_ip || "-"} + }, + { header: "Message", accessor: (row) => row.message } + ]; + + return ( +
+
+

+ Network Events + {isLoading && } +

+
+ + + + Recent Events ({data.length}) + + + { + const q = query.toLowerCase(); + return ( + (row.event_type || "").toLowerCase().includes(q) || + (row.severity || "").toLowerCase().includes(q) || + (row.source_ip || "").toLowerCase().includes(q) || + (row.message || "").toLowerCase().includes(q) + ); + }} + /> + + +
+ ); +} diff --git a/src/app/flows/page.tsx b/src/app/flows/page.tsx new file mode 100644 index 0000000..1586e70 --- /dev/null +++ b/src/app/flows/page.tsx @@ -0,0 +1,71 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useFlows } from "@/lib/api"; +import { DataTable, Column } from "@/components/ui/DataTable"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card"; +import { Loader2 } from "lucide-react"; +import { fmtBytes } from "@/lib/utils"; + +export default function FlowsPage() { + const [mounted, setMounted] = useState(false); + const { data, isLoading } = useFlows(100); // fetch 100 flows + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) return null; + + const columns: Column[] = [ + { header: "#", accessor: (row, i) => i + 1 }, + { header: "Flow ID", accessor: (row) => row.flow_id }, + { header: "Src IP", accessor: (row) => {row.src_ip} }, + { header: "Dst IP", accessor: (row) => {row.dst_ip} }, + { header: "Dst Port", accessor: (row) => row.dst_port }, + { header: "Protocol", accessor: (row) => row.protocol }, + { header: "App / Domain", accessor: (row) => row.app_label || row.domain || "-" }, + { + header: "Download", + accessor: (row) => {fmtBytes(row.bytes_download)} + }, + { + header: "Upload", + accessor: (row) => {fmtBytes(row.bytes_upload)} + } + ]; + + return ( +
+
+

+ Active Flows + {isLoading && } +

+
+ + + + Active Flows ({data.length}) + + + { + 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) + ); + }} + /> + + +
+ ); +} diff --git a/src/app/geography/page.tsx b/src/app/geography/page.tsx new file mode 100644 index 0000000..fb8bc23 --- /dev/null +++ b/src/app/geography/page.tsx @@ -0,0 +1,101 @@ +"use client"; + +import { useState, useEffect, useMemo } from "react"; +import { useCountries } from "@/lib/api"; +import { DataTable, Column } from "@/components/ui/DataTable"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card"; +import { Loader2 } from "lucide-react"; +import { fmtBytes } from "@/lib/utils"; +import { WorldMap } from "@/components/ui/WorldMap"; + +export default function GeographyPage() { + const [mounted, setMounted] = useState(false); + const { data, isLoading } = useCountries(50); // fetch 50 countries + + useEffect(() => { + setMounted(true); + }, []); + + const mapData = useMemo(() => { + if (!data) return []; + return data.map((row) => ({ + countryCode: row.country_code, + value: row.download + row.upload, + label: row.country_name, + })); + }, [data]); + + if (!mounted) return null; + + const columns: Column[] = [ + { header: "#", accessor: (row, i) => i + 1 }, + { + header: "Country", + accessor: (row) => ( +
+ {row.country_code && ( + {row.country_code} { + e.currentTarget.style.display = 'none'; + }} + /> + )} + {row.country_name} +
+ ) + }, + { header: "Country Code", accessor: (row) => row.country_code }, + { + header: "Download", + accessor: (row) => {fmtBytes(row.download)} + }, + { + header: "Upload", + accessor: (row) => {fmtBytes(row.upload)} + }, + { + header: "Total Data", + accessor: (row) => {fmtBytes(row.download + row.upload)} + } + ]; + + return ( +
+
+
+

+ Geographic Traffic + {isLoading && } +

+

Visualize data flow and activity across the globe.

+
+
+ + + + + + + + + + Traffic by Country ({data.length}) + + + { + const q = query.toLowerCase(); + return (row.country_name || "").toLowerCase().includes(q); + }} + /> + + +
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..9aff578 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,26 +1,77 @@ @import "tailwindcss"; -:root { - --background: #ffffff; - --foreground: #171717; -} - @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); -} + + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; + --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); + + --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; +} + body { - background: var(--background); + background-color: var(--background); color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; + font-family: var(--font-sans), sans-serif; + background-image: + radial-gradient(at 0% 0%, rgba(59, 130, 246, 0.15) 0px, transparent 50%), + radial-gradient(at 100% 0%, rgba(139, 92, 246, 0.15) 0px, transparent 50%); + background-attachment: fixed; } diff --git a/src/app/intelligence/page.tsx b/src/app/intelligence/page.tsx new file mode 100644 index 0000000..5153634 --- /dev/null +++ b/src/app/intelligence/page.tsx @@ -0,0 +1,252 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useIntelligenceStats } from "@/lib/api"; +import { + useIntelCryptoMining, useIntelTorDetection, useIntelVpnDetection, + useIntelIpReputation, useIntelInsecureProtocols, useIntelUnencryptedPasswords, + useIntelEncryptionAudit, useIntelDeviceDiscovery, useIntelServerDiscovery +} from "@/lib/api-advanced"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card"; +import { DataTable, Column } from "@/components/ui/DataTable"; +import { Loader2, Bitcoin, Shield, Server, Lock, AlertTriangle, Key, Globe, Network, ShieldAlert } from "lucide-react"; + +export default function IntelligencePage() { + const [mounted, setMounted] = useState(false); + const { data: stats, isLoading: loadingStats } = useIntelligenceStats(); + const [activeTab, setActiveTab] = useState("Crypto Mining"); + + // 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); + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) return null; + + const intelItems = [ + { + title: "Crypto Mining", + value: stats?.intel_crypto_mining || 0, + icon: , + description: "Devices actively mining cryptocurrency" + }, + { + title: "Tor Detection", + value: stats?.intel_tor_detection || 0, + icon: , + description: "Connections using the Tor network" + }, + { + title: "VPN Detection", + value: stats?.intel_vpn_detection || 0, + icon: , + description: "Active Virtual Private Network tunnels" + }, + { + title: "IP Reputation", + value: stats?.intel_ip_reputation || 0, + icon: , + description: "Connections to malicious IP addresses" + }, + { + title: "Insecure Protocols", + value: stats?.intel_insecure_protocols || 0, + icon: , + description: "Legacy clear-text protocols" + }, + { + title: "Unencrypted Passwords", + value: stats?.intel_unencrypted_passwords || 0, + icon: , + description: "Credentials transmitted in plain text" + }, + { + title: "Encryption Audit", + value: stats?.intel_encryption_audit || 0, + icon: , + description: "Encrypted connections audited" + }, + { + title: "Device Discovery", + value: stats?.intel_device_discovery || 0, + icon: , + description: "Unique client devices identified" + }, + { + title: "Server Discovery", + value: stats?.intel_server_discovery || 0, + icon: , + description: "Local servers running services" + } + ]; + + 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 }, + { header: "Country", accessor: (row: any) => row.country }, + ]} + /> + ); + case "IP Reputation": + return ( + 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 ( + row.ip_address }, + { header: "Encrypted %", accessor: (row) => `${row.encrypted_pct}%` }, + { header: "Risk Level", accessor: (row: any) => row.risk_level }, + ]} + /> + ); + case "Device Discovery": + return ( + row.ip_address }, + { header: "Device Type", accessor: (row: any) => row.device_type }, + { header: "OS", accessor: (row: any) => row.os_label }, + { header: "Manufacturer", accessor: (row: any) => row.manufacturer }, + ]} + /> + ); + case "Server Discovery": + return ( + row.ip_address }, + { header: "Server Type", accessor: (row: any) => row.server_type }, + { header: "Port", accessor: (row: any) => row.port }, + { header: "OS", accessor: (row: any) => row.os_label }, + ]} + /> + ); + default: return null; + } + }; + + return ( +
+
+

+ Deep Packet Inspection Intelligence + {loadingStats && } +

+
+ +
+ {intelItems.map((item, idx) => { + const isActive = activeTab === item.title; + return ( + setActiveTab(item.title)} + className={` + bg-card/50 backdrop-blur-sm border-border/50 hover:bg-card/80 transition-all cursor-pointer + ${isActive ? 'ring-2 ring-primary border-transparent' : ''} + `} + > + + {item.title} + {item.icon} + + +
+ {item.value.toLocaleString()} +
+

+ {item.description} +

+
+
+ ); + })} +
+ + + + Detailed Report: {activeTab} + + + {renderActiveTable()} + + +
+ ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 976eb90..ac86c3d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,20 +1,16 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; +import { Inter } from "next/font/google"; import "./globals.css"; +import { DashboardLayout } from "@/components/layout/DashboardLayout"; -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", +const inter = Inter({ + variable: "--font-inter", subsets: ["latin"], }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Netify Dashboard", + description: "Netify Network Intelligence Dashboard", }; export default function RootLayout({ @@ -25,9 +21,11 @@ export default function RootLayout({ return ( - {children} + + {children} + ); } diff --git a/src/app/lookup/page.tsx b/src/app/lookup/page.tsx new file mode 100644 index 0000000..fd103ab --- /dev/null +++ b/src/app/lookup/page.tsx @@ -0,0 +1,189 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useLookupApplications, LookupApp } from "@/lib/api"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card"; +import { Loader2, Search, AppWindow, HelpCircle, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-react"; + +export default function LookupPage() { + const [mounted, setMounted] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const [debouncedSearch, setDebouncedSearch] = useState(""); + const [page, setPage] = useState(1); + const limit = 24; // 24 items per page fits a 2, 3, or 4 column grid perfectly + + useEffect(() => { + setMounted(true); + }, []); + + // 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); + + if (!mounted) return null; + + const applications = data?.applications || []; + const pagination = data?.pagination || { total_records: 0, total_pages: 1, current_page: 1 }; + + return ( +
+
+

+ Application Database + {isLoading && } +

+

+ Search Netify's extensive classification database of over 2,500+ apps, protocols, and services. +

+
+ + {/* Search Input Bar */} +
+ + setSearchQuery(e.target.value)} + /> +
+ + {/* 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) => ( + +
+ {/* Top: Icon + Title */} +
+
+
+ {app.favicon || app.logo ? ( + {app.label} { + // If image fails, replace with default icon + (e.target as HTMLElement).style.display = 'none'; + }} + /> + ) : ( + + )} +
+ + {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 */} + +
+
+ )} +
+ )} +
+ ); +} diff --git a/src/app/network-infrastructure/page.tsx b/src/app/network-infrastructure/page.tsx new file mode 100644 index 0000000..63412e0 --- /dev/null +++ b/src/app/network-infrastructure/page.tsx @@ -0,0 +1,225 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Tabs, Tab } from "@/components/ui/Tabs"; +import { DataTable, Column } from "@/components/ui/DataTable"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card"; +import { + useRegions, useCities, useVlans, useInterfaces, + useIpVersions, useRemoteIps, useMacBandwidth, + useFlowTypes, useFlowOrigins +} from "@/lib/api-advanced"; + +export default function NetworkInfrastructurePage() { + // Routing & Geography Tab + const { data: regions, isLoading: loadingRegions } = useRegions(20); + const { data: cities, isLoading: loadingCities } = useCities(20); + + // Interfaces & VLANs Tab + const { data: interfaces, isLoading: loadingInterfaces } = useInterfaces(20); + const { data: vlans, isLoading: loadingVlans } = useVlans(20); + + // IP & MAC Tab + const { data: remoteIps, isLoading: loadingRemoteIps } = useRemoteIps(20); + const { data: ipVersions, isLoading: loadingIpVersions } = useIpVersions(20); + const { data: macBandwidth, isLoading: loadingMacBandwidth } = useMacBandwidth(20); + + // Flow Profiles Tab + const { data: flowTypes, isLoading: loadingFlowTypes } = useFlowTypes(20); + const { data: flowOrigins, isLoading: loadingFlowOrigins } = useFlowOrigins(20); + + const formatBytes = (bytes: number) => { + if (bytes === 0 || !bytes) return "0 B"; + const k = 1024; + const sizes = ["B", "KB", "MB", "GB", "TB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; + }; + + return ( +
+
+

Network Infrastructure

+

Physical, logical, and geographic network topologies.

+
+ + + +
+ + + Top Regions + + + row.region_name }, + { header: "Country", accessor: (row: any) => row.country_name }, + { header: "Download", accessor: (row) => formatBytes(row.download) }, + ]} + /> + + + + + Top Cities + + + row.city_name }, + { header: "Region", accessor: (row: any) => row.region_name }, + { header: "Country", accessor: (row: any) => row.country_name }, + { header: "Download", accessor: (row) => formatBytes(row.download) }, + ]} + /> + + +
+
+ + +
+ + + Network Interfaces + + + row.iface_name }, + { header: "Role", accessor: (row: any) => row.iface_role }, + { header: "Download", accessor: (row) => formatBytes(row.download) }, + { header: "Upload", accessor: (row) => formatBytes(row.upload) }, + { header: "Total", accessor: (row) => formatBytes(row.total) }, + ]} + /> + + + + + VLANs + + + row.vlan_id }, + { header: "Label", accessor: (row: any) => row.vlan_label }, + { header: "Download", accessor: (row) => formatBytes(row.download) }, + { header: "Upload", accessor: (row) => formatBytes(row.upload) }, + { header: "Total", accessor: (row) => formatBytes(row.total) }, + ]} + /> + + +
+
+ + +
+
+ + + Top Remote IPs + + + row.remote_ip }, + { header: "IP Version", accessor: (row) => `IPv${row.ip_version}` }, + { header: "Download", accessor: (row) => formatBytes(row.download) }, + { header: "Upload", accessor: (row) => formatBytes(row.upload) }, + { header: "Total", accessor: (row) => formatBytes(row.total) }, + ]} + /> + + +
+
+ + + IP Versions + + + row.ip_version_label }, + { header: "Total Traffic", accessor: (row) => formatBytes(row.total) }, + ]} + /> + + + + + MAC Bandwidth + + + row.mac_address }, + { header: "Manufacturer", accessor: (row: any) => row.manufacturer }, + { header: "Total", accessor: (row) => formatBytes(row.total) }, + ]} + /> + + +
+
+
+ + +
+ + + Flow Types + + + row.flow_type_label }, + { header: "Download", accessor: (row) => formatBytes(row.download) }, + { header: "Upload", accessor: (row) => formatBytes(row.upload) }, + { header: "Total", accessor: (row) => formatBytes(row.total) }, + ]} + /> + + + + + Flow Origins + + + row.flow_origin_label }, + { header: "Download", accessor: (row) => formatBytes(row.download) }, + { header: "Upload", accessor: (row) => formatBytes(row.upload) }, + { header: "Total", accessor: (row) => formatBytes(row.total) }, + ]} + /> + + +
+
+
+
+ ); +} diff --git a/src/app/network-intelligence/page.tsx b/src/app/network-intelligence/page.tsx new file mode 100644 index 0000000..7abf627 --- /dev/null +++ b/src/app/network-intelligence/page.tsx @@ -0,0 +1,119 @@ +"use client"; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card"; +import { DataTable, Column } from "@/components/ui/DataTable"; +import { useState, useEffect } from "react"; +import { + BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer, + PieChart, Pie, Cell +} from "recharts"; +import { Activity, Loader2 } from "lucide-react"; +import { useAppCategories, useContinents, AppCategoryStat } from "@/lib/api"; +import { fmtBytes } from "@/lib/utils"; + +const COLORS = ['#58a6ff', '#3fb950', '#f0883e', '#bc8cff', '#f85149']; + +export default function NetworkIntelligencePage() { + const [mounted, setMounted] = useState(false); + const appCategories = useAppCategories(); + const continents = useContinents(); + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) return null; + + const appCols: Column[] = [ + { header: "#", accessor: (row, i) => i + 1 }, + { header: "Category", accessor: (row) => {row.category_label || '-'} }, + { header: "Download", accessor: (row) => {fmtBytes(row.download)} }, + { header: "Upload", accessor: (row) => {fmtBytes(row.upload)} }, + { header: "Total", accessor: (row) => fmtBytes(row.total) }, + ]; + + const isLoading = appCategories.isLoading || continents.isLoading; + + return ( +
+
+
+

+ Network Intelligence + {isLoading && } +

+

Deep packet inspection analytics and geography.

+
+
+ + DPI Engine Active +
+
+ +
+ {/* App Categories */} + + + App Categories + + +
+ + + + {(appCategories.data || []).map((entry, index) => ( + + ))} + + fmtBytes(val as number)} + /> + + +
+ +
+
+ + {/* Continents */} + + + Continents (Download Volume) + + +
+ + + + `${(v/1000000000).toFixed(1)}GB`} /> + + fmtBytes(val as number)} + /> + + + +
+
+

+ Most of your traffic is bound for {continents.data && continents.data.length > 0 ? continents.data[0].continent_name : "unknown destinations"}. Ensure your firewall rules are optimized for latency across trans-oceanic cables. +

+
+
+
+
+
+ ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 3f36f7c..2b59ba3 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,65 +1,302 @@ -import Image from "next/image"; +"use client"; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card"; +import { Activity, Download, Upload, ShieldAlert, Zap, Server, Loader2 } from "lucide-react"; +import { + AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer, + BarChart, Bar, + PieChart, + Pie, + Cell +} from "recharts"; +import { useState, useEffect } from "react"; +import { useDashboardSummary, useTimeline, useTopApps, useTopProtocols } from "@/lib/api"; +import { fmtBytes } from "@/lib/utils"; + +const COLORS = ['#58a6ff', '#3fb950', '#f0883e', '#bc8cff', '#f85149']; + +export default function SummaryPage() { + const [mounted, setMounted] = useState(false); + const summary = useDashboardSummary(); + const timeline = useTimeline(24); + const topApps = useTopApps(7); + const topProtocols = useTopProtocols(); + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) return null; + + const isLoading = summary.isLoading || timeline.isLoading || topApps.isLoading || topProtocols.isLoading; -export default function Home() { return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

+
+
+
+

+ Summary Overview + {isLoading && } +

+

Real-time network traffic and intelligence summary.

- + + {/* KPI Cards */} +
+ +
+ +
+ + Download + + + +
+ {summary.data ? fmtBytes(summary.data.bandwidth_down) : "0 B"} +
+
+
+ +
+ +
+ + Upload + + + +
+ {summary.data ? fmtBytes(summary.data.bandwidth_up) : "0 B"} +
+
+
+ +
+ +
+ + Devices + + + +
+ {summary.data?.total_devices?.toLocaleString() || "0"} +
+
+
+ +
+ +
+ + Flows + + + +
+ {summary.data?.active_flows?.toLocaleString() || "0"} +
+
+
+ +
+
+ +
+ + Threats + + + +
+ {summary.data?.total_threats?.toLocaleString() || "0"} +
+
+ + +
+ +
+ + Events + + + +
+ {summary.data?.total_events?.toLocaleString() || "0"} +
+
+
+
+ +
+ {/* Timeline Chart */} + + + Bandwidth Timeline + + +
+ + + + + + + + + + + + + + { + try { + return new Date(val).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + } catch (e) { + return val; + } + }} + /> + { + if (val === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(val) / Math.log(k)); + return parseFloat((val / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; + }} + /> + { + try { + return new Date(label).toLocaleString(); + } catch (e) { + return label; + } + }} + formatter={(value: any, name: any) => [fmtBytes(value as number), name === 'total_download' ? 'Download' : 'Upload']} + /> + + + + +
+
+
+ + {/* Top Apps & Protocols */} +
+ + + Top Apps (Download) + + +
+ + + + + { + const mb = value / 1048576; + return `${mb.toFixed(0)}M`; + }} + /> + [fmtBytes(value as number), 'Download']} + /> + + + +
+
+
+ + + + Top Protocols + + +
+ + + + {topProtocols.data.map((entry, index) => ( + + ))} + + + + + {/* Custom Legend */} +
+ {topProtocols.data.slice(0, 5).map((entry, index) => ( +
+
+ {entry.protocol_label} +
+ ))} +
+
+ +
-
+
); } diff --git a/src/app/security-audit/page.tsx b/src/app/security-audit/page.tsx new file mode 100644 index 0000000..6190879 --- /dev/null +++ b/src/app/security-audit/page.tsx @@ -0,0 +1,192 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useTlsVersions, useTlsCiphers, useTlsSecurity } from "@/lib/api"; +import { DataTable, Column } from "@/components/ui/DataTable"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card"; +import { Loader2, ShieldCheck, ShieldAlert, ShieldX } from "lucide-react"; +import { fmtBytes } from "@/lib/utils"; +import { PieChart, Pie, Cell, Tooltip as RechartsTooltip, ResponsiveContainer, BarChart, Bar, XAxis, YAxis, CartesianGrid } from "recharts"; + +const COLORS = { + Recommended: "#3fb950", + Secure: "#58a6ff", + Weak: "#f0883e", + Insecure: "#f85149" +}; + +export default function SecurityAuditPage() { + const [mounted, setMounted] = useState(false); + const versions = useTlsVersions(10); + const ciphers = useTlsCiphers(20); + const security = useTlsSecurity(10); + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) return null; + + const isLoading = versions.isLoading || ciphers.isLoading || security.isLoading; + + const versionColumns: Column[] = [ + { header: "#", accessor: (row, i) => i + 1 }, + { header: "TLS Version", accessor: (row) => row.tls_version }, + { header: "Download", accessor: (row) => {fmtBytes(row.download)} }, + { header: "Upload", accessor: (row) => {fmtBytes(row.upload)} }, + { header: "Total Volume", accessor: (row) => {fmtBytes(row.total)} } + ]; + + const cipherColumns: Column[] = [ + { header: "#", accessor: (row, i) => i + 1 }, + { header: "Cipher Suite", accessor: (row) => {row.tls_cipher} }, + { header: "Download", accessor: (row) => {fmtBytes(row.download)} }, + { header: "Upload", accessor: (row) => {fmtBytes(row.upload)} }, + { header: "Total Volume", accessor: (row) => {fmtBytes(row.total)} } + ]; + + return ( +
+
+

+ Security Audit (TLS) + {isLoading && } +

+
+ + {/* Security Overview Cards */} +
+ {security.data.map((item) => ( + + + + {item.tls_security} Connections + + {item.tls_security === "Recommended" && } + {item.tls_security === "Secure" && } + {item.tls_security === "Weak" && } + {item.tls_security === "Insecure" && } + + +
+ {fmtBytes(item.total)} +
+
+
+ ))} +
+ +
+ {/* TLS Security Pie Chart */} + + + Security Level Distribution + + +
+ + + + {security.data.map((entry, index) => ( + + ))} + + [fmtBytes(value as number), 'Volume']} + /> + + +
+ {security.data.map((entry, index) => ( +
+
+ {entry.tls_security} +
+ ))} +
+
+ + + + {/* TLS Versions Bar Chart */} + + + TLS Versions + + +
+ + + + + { + const gb = value / 1073741824; + const mb = value / 1048576; + return gb >= 1 ? `${gb.toFixed(1)}GB` : `${mb.toFixed(0)}MB`; + }} + /> + [fmtBytes(value as number), 'Volume']} + /> + + + +
+
+
+
+ +
+ {/* TLS Versions Table */} + + + TLS Versions Overview + + + (row.tls_version || "").toLowerCase().includes(query.toLowerCase())} + /> + + + + {/* TLS Ciphers Table */} + + + Top TLS Cipher Suites + + + (row.tls_cipher || "").toLowerCase().includes(query.toLowerCase())} + /> + + +
+
+ ); +} diff --git a/src/app/threats/page.tsx b/src/app/threats/page.tsx new file mode 100644 index 0000000..ecd8d5a --- /dev/null +++ b/src/app/threats/page.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card"; +import { DataTable, Column } from "@/components/ui/DataTable"; +import { Badge } from "@/components/ui/Badge"; +import { useState, useEffect } from "react"; +import { ShieldCheck, Loader2 } from "lucide-react"; +import { useThreats, ThreatStat } from "@/lib/api"; + +export default function ThreatsPage() { + const [mounted, setMounted] = useState(false); + const threats = useThreats(100); + + useEffect(() => { + setMounted(true); + }, []); + + 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: "#", accessor: (row, i) => i + 1 }, + { header: "Type", accessor: (row) => row.threat_type || 'Unknown' }, + { header: "Severity", accessor: (row) => getSeverityBadge(row.severity) }, + { header: "Source IP", accessor: (row) => {row.ip_address || '-'} }, + { header: "Dest IP", accessor: (row) => row.dst_ip || '-' }, + { header: "MAC Address", accessor: (row) => row.mac_address ? {row.mac_address} : '-' }, + { header: "App", accessor: (row) => row.app_label || '-' }, + { header: "Domain", accessor: (row) => row.domain || '-' }, + { header: "Time", accessor: (row) => {row.detected_at ? new Date(row.detected_at).toLocaleString() : '-'} }, + ]; + + return ( +
+
+
+

+ Threat Intelligence + {threats.isLoading && } +

+

Monitor and respond to network security threats.

+
+
+ + Active Monitoring +
+
+ + + + Detected Threats {threats.data ? `(${threats.data.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); + }} + /> + + +
+ ); +} diff --git a/src/components/layout/DashboardLayout.tsx b/src/components/layout/DashboardLayout.tsx new file mode 100644 index 0000000..a835ddf --- /dev/null +++ b/src/components/layout/DashboardLayout.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { ReactNode, useState, useEffect } from "react"; +import { Sidebar } from "./Sidebar"; +import { Header } from "./Header"; + +export function DashboardLayout({ children }: { children: ReactNode }) { + const [isRefreshing, setIsRefreshing] = useState(false); + const [lastUpdate, setLastUpdate] = useState("—"); + + useEffect(() => { + setLastUpdate(new Date().toLocaleString('id-ID')); + }, []); + + const handleRefresh = async () => { + setIsRefreshing(true); + // Simulate API refresh + await new Promise((resolve) => setTimeout(resolve, 1000)); + setLastUpdate(new Date().toLocaleString('id-ID')); + setIsRefreshing(false); + }; + + return ( +
+ +
+
+
+
+ {children} +
+
+
+
+ ); +} diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx new file mode 100644 index 0000000..3c748ca --- /dev/null +++ b/src/components/layout/Header.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { Bell, RefreshCw } from "lucide-react"; + +export function Header({ + lastUpdate = "—", + isRefreshing = false, + onRefresh = () => {}, +}: { + lastUpdate?: string; + isRefreshing?: boolean; + onRefresh?: () => void; +}) { + return ( +
+
+
+
+ Site: {process.env.NEXT_PUBLIC_SITE_ID || "Unknown Site"} +
+
+
+
+ Update: {lastUpdate} +
+ + +
+
+ ); +} diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx new file mode 100644 index 0000000..bb93142 --- /dev/null +++ b/src/components/layout/Sidebar.tsx @@ -0,0 +1,96 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import { + Activity, + AppWindow, + Globe, + LayoutDashboard, + Shield, + ShieldAlert, + Server, + Zap, + Radio, + Lock, + Search +} from "lucide-react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +const navigation = [ + { name: "Summary", href: "/", icon: LayoutDashboard }, + { name: "Agents", href: "/agents", icon: Server }, + { name: "Devices", href: "/devices", icon: Server }, + { name: "Flows", href: "/flows", icon: Activity }, + { name: "Apps", href: "/apps", icon: AppWindow }, + { name: "App Database", href: "/lookup", icon: Search }, + { name: "Threats", href: "/threats", icon: ShieldAlert }, + { name: "Geography", href: "/geography", icon: Globe }, + { name: "DNS", href: "/dns", icon: Radio }, + { name: "Events", href: "/events", icon: Zap }, + { name: "Network Intelligence", href: "/network-intelligence", icon: Activity }, + { name: "Security Audit", href: "/security-audit", icon: Lock }, + { name: "Intelligence", href: "/intelligence", icon: Shield }, + { name: "Network Infra", href: "/network-infrastructure", icon: Server }, + { name: "DPI Analytics", href: "/dpi-analytics", icon: Activity }, +]; + +export function Sidebar() { + const pathname = usePathname(); + + return ( +
+ {/* Subtle glow effect behind sidebar */} +
+ +
+
+ +
+ + Netify + +
+
+ +
+
+ ); +} diff --git a/src/components/ui/Badge.tsx b/src/components/ui/Badge.tsx new file mode 100644 index 0000000..73abfe5 --- /dev/null +++ b/src/components/ui/Badge.tsx @@ -0,0 +1,26 @@ +import { cn } from "@/lib/utils"; + +interface BadgeProps extends React.HTMLAttributes { + variant?: "default" | "red" | "orange" | "yellow" | "green" | "blue" | "cyan"; +} + +export function Badge({ className, variant = "default", ...props }: BadgeProps) { + return ( +
+ ); +} diff --git a/src/components/ui/Card.tsx b/src/components/ui/Card.tsx new file mode 100644 index 0000000..6df421e --- /dev/null +++ b/src/components/ui/Card.tsx @@ -0,0 +1,68 @@ +import { cn } from "@/lib/utils"; +import { ReactNode } from "react"; + +export function Card({ + children, + className, + ...props +}: React.HTMLAttributes & { + children: ReactNode; + className?: string; +}) { + return ( +
+ {children} +
+ ); +} + +export function CardHeader({ + children, + className, +}: { + children: ReactNode; + className?: string; +}) { + return ( +
+ {children} +
+ ); +} + +export function CardTitle({ + children, + className, +}: { + children: ReactNode; + className?: string; +}) { + return ( +

+ {children} +

+ ); +} + +export function CardContent({ + children, + className, +}: { + children: ReactNode; + className?: string; +}) { + return ( +
{children}
+ ); +} diff --git a/src/components/ui/DataTable.tsx b/src/components/ui/DataTable.tsx new file mode 100644 index 0000000..83aaf35 --- /dev/null +++ b/src/components/ui/DataTable.tsx @@ -0,0 +1,102 @@ +"use client"; + +import { useState } from "react"; +import { Search } from "lucide-react"; + +export interface Column { + header: string; + accessor: (row: T, index: number) => React.ReactNode; + className?: string; +} + +interface DataTableProps { + data: T[]; + columns: Column[]; + searchPlaceholder?: string; + searchFilter?: (row: T, query: string) => boolean; + isLoading?: boolean; +} + +export function DataTable({ + data, + columns, + searchPlaceholder = "Search...", + searchFilter, + isLoading +}: DataTableProps) { + const [query, setQuery] = useState(""); + + const filteredData = searchFilter + ? data.filter(row => searchFilter(row, query)) + : data; + + return ( +
+ {searchFilter && ( +
+ + setQuery(e.target.value)} + className="w-full max-w-sm rounded-lg border border-border bg-card/30 backdrop-blur-sm pl-9 pr-4 py-2.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" + /> +
+ )} +
+
+ + + + {columns.map((col, i) => ( + + ))} + + + + {isLoading ? ( + + + + ) : filteredData.length === 0 ? ( + + + + ) : ( + filteredData.map((row, i) => ( + + {columns.map((col, j) => ( + + ))} + + )) + )} + +
+ {col.header} +
+
+
+
+ + + + +
+ Loading data... +
+
+
+ + No data found + {query && Try adjusting your search query} +
+
+ {col.accessor(row, i)} +
+
+
+
+ ); +} diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx new file mode 100644 index 0000000..7352a50 --- /dev/null +++ b/src/components/ui/Modal.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { ReactNode, useEffect, useState } from "react"; +import { X } from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface ModalProps { + isOpen: boolean; + onClose: () => void; + title: string; + children: ReactNode; + className?: string; +} + +export function Modal({ isOpen, onClose, title, children, className }: ModalProps) { + const [isMounted, setIsMounted] = useState(false); + + useEffect(() => { + setIsMounted(true); + if (isOpen) { + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = "unset"; + } + return () => { + document.body.style.overflow = "unset"; + }; + }, [isOpen]); + + if (!isMounted || !isOpen) return null; + + return ( +
+ {/* Backdrop */} +
+ + {/* Dialog */} +
+
+

{title}

+ +
+
+ {children} +
+
+
+ ); +} diff --git a/src/components/ui/Tabs.tsx b/src/components/ui/Tabs.tsx new file mode 100644 index 0000000..45880fb --- /dev/null +++ b/src/components/ui/Tabs.tsx @@ -0,0 +1,49 @@ +import React, { useState } from 'react'; + +interface TabProps { + label: string; + children: React.ReactNode; +} + +export function Tab({ children }: TabProps) { + return <>{children}; +} + +interface TabsProps { + children: React.ReactElement[]; +} + +export function Tabs({ children }: TabsProps) { + const [activeTab, setActiveTab] = useState(0); + + return ( +
+
+ {React.Children.map(children, (child, index) => { + if (!React.isValidElement(child)) return null; + const isActive = index === activeTab; + return ( + + ); + })} +
+
+ {React.Children.map(children, (child, index) => { + if (index !== activeTab) return null; + return child; + })} +
+
+ ); +} diff --git a/src/components/ui/WorldMap.tsx b/src/components/ui/WorldMap.tsx new file mode 100644 index 0000000..45ebbf3 --- /dev/null +++ b/src/components/ui/WorldMap.tsx @@ -0,0 +1,141 @@ +"use client"; + +import React, { memo, useState, useEffect } from "react"; +import { + ComposableMap, + Geographies, + Geography, + Sphere, + Graticule, + ZoomableGroup +} from "react-simple-maps"; +import { scaleLinear } from "d3-scale"; +import { fmtBytes } from "@/lib/utils"; + +// The topojson file containing the world shapes +const geoUrl = "https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json"; + +// Our custom heatmap color scale +// Maps values from 0 to maxValue to a gradient (Dark Blue to Electric Cyan/Purple) +const colorScale = scaleLinear() + .domain([0, 100]) // We will update the domain dynamically based on data + .range(["#1e293b", "#38bdf8"]); // Tailwind slate-800 to sky-400 + +interface WorldMapProps { + data: { + countryCode: string; // ISO A2 code + value: number; // The value used for heatmap + label: string; // Country name for tooltip + }[]; +} + +const WorldMapComponent = ({ data }: WorldMapProps) => { + const [tooltipContent, setTooltipContent] = useState(""); + const [maxValue, setMaxValue] = useState(0); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + if (data.length > 0) { + const max = Math.max(...data.map(d => d.value)); + setMaxValue(max); + colorScale.domain([0, max]); + } + }, [data]); + + if (!mounted) return null; + + return ( +
+
+
+ Heatmap Activity +
+
+
Low
+
+
High
+
+
+ + {/* Tooltip Overlay */} + {tooltipContent && ( +
+
{tooltipContent.split(':')[0]}
+
{tooltipContent.split(':')[1]}
+
+ )} + + + + + + + + {({ geographies }) => + geographies.map((geo) => { + // geo.properties.name contains the country name in this TopoJSON + // Try to find if we have data for this country + const d = data.find((s) => { + if (!geo.properties.name) return false; + const geoName = geo.properties.name.toLowerCase(); + const sLabel = s.label.toLowerCase(); + + return geoName === sLabel || + (sLabel === 'united states' && geoName === 'united states of america') || + (sLabel === 'the netherlands' && geoName === 'netherlands') || + (sLabel === 'south korea' && geoName === 'korea'); + }); + + const color = d ? colorScale(d.value) : "#1e293b"; // Default color + + return ( + { + if (d) { + setTooltipContent(`${d.label}: ${fmtBytes(d.value)}`); + } else { + setTooltipContent(`${geo.properties.name}: No data`); + } + }} + onMouseLeave={() => { + setTooltipContent(""); + }} + /> + ); + }) + } + + + +
+ ); +}; + +export const WorldMap = memo(WorldMapComponent); diff --git a/src/lib/actions/agents.ts b/src/lib/actions/agents.ts new file mode 100644 index 0000000..1021949 --- /dev/null +++ b/src/lib/actions/agents.ts @@ -0,0 +1,152 @@ +"use server"; + +import { revalidatePath } from "next/cache"; + +const API_URL = process.env.NETIFY_BASE_URL ? `${process.env.NETIFY_BASE_URL}/assets/agents` : "https://manager.netify.ai/api/v1/assets/agents"; + +// Helper function to get headers +const getHeaders = () => { + const API_KEY = process.env.NETIFY_API_KEY; + const JWT_TOKEN = process.env.NETIFY_JWT_TOKEN; + const SITE_UUID = process.env.NETIFY_SITE_UUID; + + if (!API_KEY && !JWT_TOKEN) { + throw new Error("NETIFY_API_KEY or NETIFY_JWT_TOKEN is not set in environment variables"); + } + + const headers: Record = { + "Content-Type": "application/json", + }; + + // JWT Token access requires x-net-site + if (JWT_TOKEN && SITE_UUID) { + headers["x-api-key"] = JWT_TOKEN; + headers["x-net-site"] = SITE_UUID; + } else if (API_KEY) { + headers["x-api-key"] = API_KEY; + } + + return headers; +}; + +export interface Agent { + id: number; + uuid: string; + serial: string; + site_uuid: string; + organization_uuid: string; + provisioned: boolean; + activated: boolean; + label?: string; + created_at: { human: string; date: string; unix_time: number }; + updated_at: { human: string; date: string; unix_time: number }; + last_seen_at: { human: string; date: string; unix_time: number }; +} + +export async function getAgents(): Promise { + try { + const response = await fetch(API_URL, { + method: "GET", + headers: getHeaders(), + cache: "no-store", + }); + + if (!response.ok) { + if (response.status === 403) { + throw new Error("Asset Management Disabled: The configured API key does not have permission to manage provisioning agents (403 Forbidden)."); + } + throw new Error(`Failed to fetch agents: ${response.statusText} (${response.status})`); + } + + const result = await response.json(); + if (result.status_code !== 0) { + throw new Error(result.status_message || "Unknown API error"); + } + + return result.data || []; + } catch (error: any) { + console.error("Error fetching agents:", error); + throw new Error(error.message || "Failed to retrieve agents inventory."); + } +} + +export async function getAgent(agentId: string): Promise { + try { + const response = await fetch(`${API_URL}/${agentId}`, { + method: "GET", + headers: getHeaders(), + cache: "no-store", + }); + + if (!response.ok) { + throw new Error(`Failed to fetch agent ${agentId}: ${response.statusText}`); + } + + const result = await response.json(); + return result.data; + } catch (error) { + console.error(`Error fetching agent ${agentId}:`, error); + return null; + } +} + +export async function createAgent(agentId: string, label: string) { + try { + const url = agentId + ? `${API_URL}/${encodeURIComponent(agentId)}?label=${encodeURIComponent(label)}` + : `${API_URL}?label=${encodeURIComponent(label)}`; + + const response = await fetch(url, { + method: "POST", + headers: getHeaders(), + }); + + const result = await response.json(); + if (!response.ok || result.status_code !== 0) { + return { success: false, message: result.status_message || response.statusText }; + } + + revalidatePath('/agents'); + return { success: true, data: result.data }; + } catch (error: any) { + return { success: false, message: error.message }; + } +} + +export async function updateAgent(agentId: string, label: string) { + try { + const response = await fetch(`${API_URL}/${encodeURIComponent(agentId)}?label=${encodeURIComponent(label)}`, { + method: "PATCH", + headers: getHeaders(), + }); + + const result = await response.json(); + if (!response.ok || result.status_code !== 0) { + return { success: false, message: result.status_message || response.statusText }; + } + + revalidatePath('/agents'); + return { success: true, data: result.data }; + } catch (error: any) { + return { success: false, message: error.message }; + } +} + +export async function deleteAgent(agentId: string) { + try { + const response = await fetch(`${API_URL}/${encodeURIComponent(agentId)}`, { + method: "DELETE", + headers: getHeaders(), + }); + + const result = await response.json(); + if (!response.ok || result.status_code !== 0) { + return { success: false, message: result.status_message || response.statusText }; + } + + revalidatePath('/agents'); + return { success: true }; + } catch (error: any) { + return { success: false, message: error.message }; + } +} diff --git a/src/lib/api-advanced.ts b/src/lib/api-advanced.ts new file mode 100644 index 0000000..b0076c8 --- /dev/null +++ b/src/lib/api-advanced.ts @@ -0,0 +1,85 @@ +import { useState, useEffect } from 'react'; +import { fetcher } from './api'; + +// --- ADVANCED NETWORK INFRASTRUCTURE TYPES --- +export interface RegionStat { id: number; fetched_at: string; region_name: string; region_code: string; country_name: string; country_code: string; download: number; } +export interface CityStat { id: number; fetched_at: string; city_name: string; region_name: string; country_name: string; country_code: string; download: number; } +export interface VlanStat { id: number; fetched_at: string; vlan_id: number; vlan_label: string; download: number; upload: number; total: number; } +export interface InterfaceStat { id: number; fetched_at: string; iface_id: number; iface_name: string; iface_role: string; agent_id: string; download: number; upload: number; total: number; } +export interface IpVersionStat { id: number; fetched_at: string; ip_version_label: string; download: number; upload: number; total: number; } +export interface RemoteIpStat { id: number; fetched_at: string; remote_ip: string; ip_version: number; download: number; upload: number; total: number; } +export interface MacBandwidthStat { id: number; fetched_at: string; mac_address: string; manufacturer: string; download: number; upload: number; total: number; } +export interface FlowTypeStat { id: number; fetched_at: string; flow_type_label: string; download: number; upload: number; total: number; } +export interface FlowOriginStat { id: number; fetched_at: string; flow_origin_label: string; download: number; upload: number; total: number; } + +// --- DPI ANALYTICS TYPES --- +export interface NetbiosStat { id: number; fetched_at: string; hostname: string; download: number; upload: number; total: number; } +export interface DiscoveryOsStat { id: number; fetched_at: string; os_label: string; download: number; upload: number; total: number; } +export interface DhcpFingerprintStat { id: number; fetched_at: string; fingerprint: string; download: number; upload: number; total: number; } +export interface HttpUserAgentStat { id: number; fetched_at: string; user_agent: string; download: number; upload: number; total: number; } +export interface SniHostnameStat { id: number; fetched_at: string; sni_hostname: string; download: number; upload: number; total: number; } +export interface SslServerCnStat { id: number; fetched_at: string; ssl_server_cn: string; download: number; upload: number; total: number; } +export interface QuicHostnameStat { id: number; fetched_at: string; quic_hostname: string; download: number; upload: number; total: number; } +export interface BittorrentHashStat { id: number; fetched_at: string; info_hash: string; label: string; download: number; upload: number; total: number; } +export interface SshVersionStat { id: number; fetched_at: string; ssh_version: string; download: number; upload: number; total: number; } +export interface MdnsHostnameStat { id: number; fetched_at: string; mdns_hostname: string; download: number; upload: number; total: number; } + +// --- INTELLIGENCE DETAIL TYPES --- +export interface IntelCryptoMining { id: number; fetched_at: string; detected_at: string; ip_address: string; mac_address: string; pool_host: string; pool_ip: string; protocol: string; app_label: string; confidence: number; download: number; upload: number; } +export interface IntelDeviceDiscovery { id: number; fetched_at: string; detected_at: string; ip_address: string; mac_address: string; device_label: string; device_type: string; os_label: string; manufacturer: string; is_new: number; } +export interface IntelEncryptionAudit { id: number; fetched_at: string; detected_at: string; ip_address: string; mac_address: string; device_label: string; encrypted_pct: number; unencrypted: number; encrypted: number; total: number; risk_level: string; } +export interface IntelInsecureProtocol { id: number; fetched_at: string; detected_at: string; protocol: string; ip_address: string; mac_address: string; dst_ip: string; dst_port: number; app_label: string; download: number; upload: number; risk: string; source: string; } +export interface IntelIpReputation { id: number; fetched_at: string; detected_at: string; ip_address: string; local_ip: string; mac_address: string; reputation: string; score: number; country: string; app_label: string; download: number; upload: number; blacklisted: number; } +export interface IntelServerDiscovery { id: number; fetched_at: string; detected_at: string; ip_address: string; mac_address: string; server_type: string; hostname: string; port: number; protocol: string; os_label: string; download: number; upload: number; } +export interface IntelTorDetection { id: number; fetched_at: string; detected_at: string; ip_address: string; mac_address: string; exit_node: string; circuit_id: string; download: number; upload: number; country: string; } +export interface IntelUnencryptedPassword { id: number; fetched_at: string; detected_at: string; ip_address: string; mac_address: string; dst_ip: string; dst_port: number; protocol: string; username: string; download: number; upload: number; severity: string; } +export interface IntelVpnDetection { id: number; fetched_at: string; detected_at: string; ip_address: string; mac_address: string; vpn_type: string; remote_ip: string; protocol: string; download: number; upload: number; country: string; confidence: number; } + +function useGenericFetch(endpoint: string, limit: number) { + const [data, setData] = useState([]); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + fetcher(`${endpoint}?limit=${limit}`) + .then((res) => setData(res || [])) + .catch(setError) + .finally(() => setIsLoading(false)); + }, [endpoint, limit]); + + return { data, error, isLoading }; +} + +// Network Infrastructure Hooks +export const useRegions = (limit = 50) => useGenericFetch('/regions', limit); +export const useCities = (limit = 50) => useGenericFetch('/cities', limit); +export const useVlans = (limit = 50) => useGenericFetch('/vlans', limit); +export const useInterfaces = (limit = 50) => useGenericFetch('/interfaces', limit); +export const useIpVersions = (limit = 50) => useGenericFetch('/ip-versions', limit); +export const useRemoteIps = (limit = 50) => useGenericFetch('/remote-ips', limit); +export const useMacBandwidth = (limit = 50) => useGenericFetch('/mac-bandwidth', limit); +export const useFlowTypes = (limit = 50) => useGenericFetch('/flow-types', limit); +export const useFlowOrigins = (limit = 50) => useGenericFetch('/flow-origins', limit); + +// DPI Analytics Hooks +export const useNetbios = (limit = 50) => useGenericFetch('/netbios', limit); +export const useDiscoveryOs = (limit = 50) => useGenericFetch('/discovery-os', limit); +export const useDhcpFingerprints = (limit = 50) => useGenericFetch('/dhcp-fingerprints', limit); +export const useHttpUserAgents = (limit = 50) => useGenericFetch('/http-user-agents', limit); +export const useSniHostnames = (limit = 50) => useGenericFetch('/sni-hostnames', limit); +export const useSslServerCn = (limit = 50) => useGenericFetch('/ssl-server-cn', limit); +export const useQuicHostnames = (limit = 50) => useGenericFetch('/quic-hostnames', limit); +export const useBittorrentHashes = (limit = 50) => useGenericFetch('/bittorrent-hashes', limit); +export const useSshVersions = (limit = 50) => useGenericFetch('/ssh-versions', limit); +export const useMdnsHostnames = (limit = 50) => useGenericFetch('/mdns-hostnames', limit); + +// Intelligence Detail Hooks +export const useIntelCryptoMining = (limit = 50) => useGenericFetch('/intelligence/crypto-mining', limit); +export const useIntelDeviceDiscovery = (limit = 50) => useGenericFetch('/intelligence/device-discovery', limit); +export const useIntelEncryptionAudit = (limit = 50) => useGenericFetch('/intelligence/encryption-audit', limit); +export const useIntelInsecureProtocols = (limit = 50) => useGenericFetch('/intelligence/insecure-protocols', limit); +export const useIntelIpReputation = (limit = 50) => useGenericFetch('/intelligence/ip-reputation', limit); +export const useIntelServerDiscovery = (limit = 50) => useGenericFetch('/intelligence/server-discovery', limit); +export const useIntelTorDetection = (limit = 50) => useGenericFetch('/intelligence/tor', limit); +export const useIntelUnencryptedPasswords = (limit = 50) => useGenericFetch('/intelligence/unencrypted-passwords', limit); +export const useIntelVpnDetection = (limit = 50) => useGenericFetch('/intelligence/vpn', limit); diff --git a/src/lib/api.ts b/src/lib/api.ts new file mode 100644 index 0000000..c5d2879 --- /dev/null +++ b/src/lib/api.ts @@ -0,0 +1,460 @@ +import { useState, useEffect } from 'react'; + +// Common Response Wrapper +export interface ApiResponse { + ok: boolean; + data: T; + message?: string; +} + +// Data Types +export interface FlowStat { + id: number; + fetched_at: string; + flow_id: string; + src_ip: string; + src_mac: string | null; + dst_ip: string; + dst_port: number; + protocol: string; + app_label: string | null; + domain: string | null; + bytes_download: number; + bytes_upload: number; + first_seen: string; + last_seen: string; +} + +export interface CountryStat { + id: number; + country_code: string; + country_name: string; + download: number; + upload: number; + flow_count: number; +} + +export interface DnsStat { + id: number; + domain: string; + query_count: number; + app_label: string | null; + category: string | null; +} + +export interface EventStat { + id: number; + event_type: string; + severity: string; + message: string; + source_ip: string | null; + timestamp: string; +} + +export interface TlsVersionStat { + id: number; + tls_version: string; + download: number; + upload: number; + total: number; +} + +export interface TlsCipherStat { + id: number; + tls_cipher: string; + download: number; + upload: number; + total: number; +} + +export interface TlsSecurityStat { + id: number; + tls_security: string; + color: string; + download: number; + upload: number; + total: number; +} + +export interface IntelligenceStats { + intel_crypto_mining: number; + intel_device_discovery: number; + intel_encryption_audit: number; + intel_insecure_protocols: number; + intel_ip_reputation: number; + intel_server_discovery: number; + intel_tor_detection: number; + intel_unencrypted_passwords: number; + intel_vpn_detection: number; +} + +export interface DashboardSummary { + total_devices: number; + total_threats: number; + total_events: number; + last_fetch: string; + bandwidth_down: number; + bandwidth_up: number; + active_flows: number; +} + +export interface TimelinePoint { + fetched_at: string; + total_download: number; + total_upload: number; +} + +export interface AppStat { + app_label: string; + download: number; + upload: number; + favicon?: string; + category?: string; +} + +export interface ProtocolStat { + protocol_label: string; + download: number; + upload: number; +} + +export interface DeviceStat { + id: number; + ip_address: string; + mac_address: string | null; + device_label: string | null; + device_type: string | null; + os_label: string | null; + manufacturer: string | null; + download: number; + upload: number; + last_seen: string; +} + +export interface ThreatStat { + id: number; + threat_type: string | null; + severity: string | null; + ip_address: string | null; + dst_ip: string | null; + mac_address: string | null; + app_label: string | null; + domain: string | null; + detected_at: string | null; +} + +export interface AppCategoryStat { + id: number; + category_label: string; + download: number; + upload: number; + total: number; +} + +export interface ContinentStat { + id: number; + continent_name: string; + download: number; + upload: number; + total: number; +} + +// Fetcher Function +export async function fetcher(endpoint: string): Promise { + const url = `/api/dashboard${endpoint}`; + const res = await fetch(url); + if (!res.ok) { + throw new Error(`API error: ${res.status} ${res.statusText}`); + } + const json = await res.json(); + if (!json.ok) { + throw new Error(json.message || 'API responded with ok: false'); + } + return json.data; +} + +// Custom Hooks for React Components +export function useDashboardSummary() { + const [data, setData] = useState(null); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + fetcher('/summary') + .then(setData) + .catch(setError) + .finally(() => setIsLoading(false)); + }, []); + + return { data, error, isLoading }; +} + +export function useTimeline(points: number = 60) { + const [data, setData] = useState([]); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + fetcher(`/timeline?points=${points}`) + .then((res) => { + // Reverse array if backend sends newest first, so oldest is first for the chart + setData(Array.isArray(res) ? res.slice().reverse() : []); + }) + .catch(setError) + .finally(() => setIsLoading(false)); + }, [points]); + + return { data, error, isLoading }; +} + +export function useTopApps(limit: number = 10) { + const [data, setData] = useState([]); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + fetcher(`/apps?limit=${limit}`) + .then((res) => setData(res || [])) + .catch(setError) + .finally(() => setIsLoading(false)); + }, [limit]); + + return { data, error, isLoading }; +} + +export function useTopProtocols() { + const [data, setData] = useState([]); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + fetcher('/protocols') + .then((res) => setData(res || [])) + .catch(setError) + .finally(() => setIsLoading(false)); + }, []); + + return { data, error, isLoading }; +} + +export function useDevices(limit: number = 50) { + const [data, setData] = useState([]); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + fetcher(`/devices?limit=${limit}`) + .then((res) => setData(res || [])) + .catch(setError) + .finally(() => setIsLoading(false)); + }, [limit]); + + return { data, error, isLoading }; +} + +export function useThreats(limit: number = 20) { + const [data, setData] = useState([]); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + fetcher(`/threats?limit=${limit}`) + .then((res) => setData(res || [])) + .catch(setError) + .finally(() => setIsLoading(false)); + }, [limit]); + + return { data, error, isLoading }; +} + +export function useAppCategories() { + const [data, setData] = useState([]); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + fetcher('/app-categories') + .then((res) => setData(res || [])) + .catch(setError) + .finally(() => setIsLoading(false)); + }, []); + + return { data, error, isLoading }; +} + +export function useContinents() { + const [data, setData] = useState([]); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + fetcher('/continents') + .then((res) => setData(res || [])) + .catch(setError) + .finally(() => setIsLoading(false)); + }, []); + + return { data, error, isLoading }; +} + +export function useFlows(limit: number = 50) { + const [data, setData] = useState([]); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + fetcher(`/flows?limit=${limit}`) + .then((res) => setData(res || [])) + .catch(setError) + .finally(() => setIsLoading(false)); + }, [limit]); + + return { data, error, isLoading }; +} + +export function useCountries(limit: number = 50) { + const [data, setData] = useState([]); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + fetcher(`/countries?limit=${limit}`) + .then((res) => setData(res || [])) + .catch(setError) + .finally(() => setIsLoading(false)); + }, [limit]); + + return { data, error, isLoading }; +} + +export function useDNS(limit: number = 50) { + const [data, setData] = useState([]); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + fetcher(`/dns?limit=${limit}`) + .then((res) => setData(res || [])) + .catch(setError) + .finally(() => setIsLoading(false)); + }, [limit]); + + return { data, error, isLoading }; +} + +export function useEvents(limit: number = 50) { + const [data, setData] = useState([]); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + fetcher(`/events?limit=${limit}`) + .then((res) => setData(res || [])) + .catch(setError) + .finally(() => setIsLoading(false)); + }, [limit]); + + return { data, error, isLoading }; +} + +export function useTlsVersions(limit: number = 10) { + const [data, setData] = useState([]); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + fetcher(`/tls-versions?limit=${limit}`) + .then((res) => setData(res || [])) + .catch(setError) + .finally(() => setIsLoading(false)); + }, [limit]); + + return { data, error, isLoading }; +} + +export function useTlsCiphers(limit: number = 15) { + const [data, setData] = useState([]); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + fetcher(`/tls-ciphers?limit=${limit}`) + .then((res) => setData(res || [])) + .catch(setError) + .finally(() => setIsLoading(false)); + }, [limit]); + + return { data, error, isLoading }; +} + +export function useTlsSecurity(limit: number = 10) { + const [data, setData] = useState([]); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + fetcher(`/tls-security?limit=${limit}`) + .then((res) => setData(res || [])) + .catch(setError) + .finally(() => setIsLoading(false)); + }, [limit]); + + return { data, error, isLoading }; +} + +export function useIntelligenceStats() { + const [data, setData] = useState(null); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + fetcher('/intelligence/stats') + .then(setData) + .catch(setError) + .finally(() => setIsLoading(false)); + }, []); + + return { data, error, isLoading }; +} + +// ─── LOOKUP APPLICATIONS ────────────────────────────────────────────────────── +export interface LookupApp { + id: number; + tag: string; + label: string; + name: string; + full_name: string; + description: string; + favicon: string; + icon: string; + logo: string; + application_category: { id: number; tag: string; label: string }; +} + +export interface LookupApplicationsResponse { + applications: LookupApp[]; + pagination: { + total_records: number; + total_pages: number; + current_page: number; + start: number; + length: number; + limit: number; + }; +} + +export function useLookupApplications(page: number = 1, limit: number = 25, search: string = '') { + const [data, setData] = useState(null); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + setIsLoading(true); + fetcher(`/lookup/applications?page=${page}&limit=${limit}&search=${encodeURIComponent(search)}`) + .then(setData) + .catch(setError) + .finally(() => setIsLoading(false)); + }, [page, limit, search]); + + return { data, error, isLoading }; +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..8b6cf74 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,13 @@ +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +export function fmtBytes(bytes?: number) { + if (!bytes || bytes === 0) return '0 B'; + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + units[i]; +}