Initialize project and add Netify API integration with TDD workflow

This commit is contained in:
Rafhan Mazaya Fathurrahman committed 2026-06-30 11:07:50 +07:00
1 parent 8a077cf692
commit da8fb57bfb
43 files changed
+9230 -115

No files matched your search

+4
View File
@@ -39,3 +39,7 @@ yarn-error.log*
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
# local databases & temporary files
backend/netify_data.db*
temp.json
+1079
View File
File diff suppressed because it is too large. Load diff
+1476
View File
File diff suppressed because it is too large. Load diff
+293
View File
@@ -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;
+282
View File
@@ -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 };
+32
View File
@@ -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();
});
+125
View File
@@ -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);
});
+8 -1
View File
@@ -1,7 +1,14 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
const nextConfig: NextConfig = { 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; export default nextConfig;
+2059 -27
View File
File diff suppressed because it is too large. Load diff
+20 -3
View File
@@ -3,21 +3,38 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "concurrently \"next dev\" \"node backend/server.js\"",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "eslint" "lint": "eslint",
"backend": "node backend/server.js"
}, },
"dependencies": { "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", "next": "16.2.9",
"node-cron": "^4.5.0",
"react": "19.2.4", "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": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"@types/d3-scale": "^4.0.9",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"@types/react-simple-maps": "^3.0.6",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.2.9", "eslint-config-next": "16.2.9",
"tailwindcss": "^4", "tailwindcss": "^4",
+14
View File
@@ -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');
+35
View File
@@ -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();
+250
View File
@@ -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<Agent[]>([]);
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<Agent | null>(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<Agent>[] = [
{ header: "UUID / Serial", accessor: (row) => <span className="font-mono text-xs text-blue-400">{row.uuid || row.serial}</span> },
{ header: "Label", accessor: (row) => row.label || <span className="text-muted-foreground italic">No label</span> },
{ header: "Provisioned", accessor: (row) => row.provisioned ? <Badge variant="green"><CheckCircle2 className="w-3 h-3 mr-1" /> Yes</Badge> : <Badge variant="red"><XCircle className="w-3 h-3 mr-1" /> No</Badge> },
{ header: "Activated", accessor: (row) => row.activated ? <Badge variant="green"><CheckCircle2 className="w-3 h-3 mr-1" /> Yes</Badge> : <Badge variant="red"><XCircle className="w-3 h-3 mr-1" /> No</Badge> },
{ header: "Last Seen", accessor: (row) => <span className="text-muted-foreground">{row.last_seen_at?.human || '-'}</span> },
{
header: "Actions",
accessor: (row) => (
<div className="flex items-center gap-2">
<button onClick={() => openEdit(row)} className="p-1.5 text-slate-400 hover:text-white hover:bg-slate-800 rounded transition-colors">
<Edit className="w-4 h-4" />
</button>
<button onClick={() => openDelete(row)} className="p-1.5 text-red-400 hover:text-red-300 hover:bg-red-950/30 rounded transition-colors">
<Trash2 className="w-4 h-4" />
</button>
</div>
)
},
];
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold tracking-tight text-white flex items-center gap-3">
<Server className="w-6 h-6 text-primary" />
Agents Inventory
{isLoading && <Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />}
</h2>
<button
onClick={openCreate}
className="flex items-center gap-2 bg-primary hover:bg-primary/90 text-primary-foreground px-4 py-2 rounded-md text-sm font-medium transition-colors"
>
<Plus className="w-4 h-4" />
Provision Agent
</button>
</div>
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
<CardHeader>
<CardTitle className="text-white">Active Agents {agents.length > 0 ? `(${agents.length})` : ''}</CardTitle>
</CardHeader>
<CardContent>
{error ? (
<div className="p-4 bg-red-950/30 border border-red-900/50 rounded-lg text-red-400 text-sm">
{error}
</div>
) : (
<DataTable
data={agents}
columns={columns}
searchPlaceholder="Search agents by UUID or label..."
searchFilter={(row, query) => {
const q = query.toLowerCase();
return (row.uuid || "").toLowerCase().includes(q) ||
(row.serial || "").toLowerCase().includes(q) ||
(row.label || "").toLowerCase().includes(q);
}}
/>
)}
</CardContent>
</Card>
{/* Create Modal */}
<Modal isOpen={isCreateOpen} onClose={() => setIsCreateOpen(false)} title="Provision New Agent">
<form onSubmit={handleCreate} className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-300">Agent UUID / Serial *</label>
<input
required
type="text"
placeholder="e.g. EX-AM-PL-ES"
value={formData.agentId}
onChange={e => 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"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-300">Agent Label</label>
<input
type="text"
placeholder="e.g. Branch Office Gateway"
value={formData.label}
onChange={e => 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"
/>
</div>
<div className="pt-4 flex justify-end gap-3">
<button type="button" onClick={() => setIsCreateOpen(false)} className="px-4 py-2 text-sm font-medium text-slate-300 hover:text-white transition-colors">Cancel</button>
<button type="submit" disabled={isSubmitting} className="flex items-center gap-2 bg-primary hover:bg-primary/90 text-primary-foreground px-4 py-2 rounded-md text-sm font-medium transition-colors disabled:opacity-50">
{isSubmitting ? <Loader2 className="w-4 h-4 animate-spin" /> : null}
Create Agent
</button>
</div>
</form>
</Modal>
{/* Edit Modal */}
<Modal isOpen={isEditOpen} onClose={() => setIsEditOpen(false)} title="Edit Agent">
<form onSubmit={handleUpdate} className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Agent UUID / Serial</label>
<input
disabled
type="text"
value={selectedAgent?.uuid || selectedAgent?.serial || ""}
className="w-full bg-slate-900/50 border border-slate-800 rounded-md px-3 py-2 text-sm text-slate-500 cursor-not-allowed"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-300">Agent Label</label>
<input
autoFocus
type="text"
placeholder="e.g. Branch Office Gateway"
value={formData.label}
onChange={e => 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"
/>
</div>
<div className="pt-4 flex justify-end gap-3">
<button type="button" onClick={() => setIsEditOpen(false)} className="px-4 py-2 text-sm font-medium text-slate-300 hover:text-white transition-colors">Cancel</button>
<button type="submit" disabled={isSubmitting} className="flex items-center gap-2 bg-primary hover:bg-primary/90 text-primary-foreground px-4 py-2 rounded-md text-sm font-medium transition-colors disabled:opacity-50">
{isSubmitting ? <Loader2 className="w-4 h-4 animate-spin" /> : null}
Save Changes
</button>
</div>
</form>
</Modal>
{/* Delete Modal */}
<Modal isOpen={isDeleteOpen} onClose={() => setIsDeleteOpen(false)} title="Confirm Deletion">
<div className="space-y-4">
<p className="text-sm text-slate-300">
Are you sure you want to delete the agent <span className="font-bold text-white">{selectedAgent?.label || selectedAgent?.uuid}</span>? This action cannot be undone.
</p>
<div className="pt-4 flex justify-end gap-3">
<button type="button" onClick={() => setIsDeleteOpen(false)} className="px-4 py-2 text-sm font-medium text-slate-300 hover:text-white transition-colors">Cancel</button>
<button onClick={handleDelete} disabled={isSubmitting} className="flex items-center gap-2 bg-destructive hover:bg-destructive/90 text-destructive-foreground px-4 py-2 rounded-md text-sm font-medium transition-colors disabled:opacity-50">
{isSubmitting ? <Loader2 className="w-4 h-4 animate-spin" /> : null}
Delete Agent
</button>
</div>
</div>
</Modal>
</div>
);
}
+70
View File
@@ -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 });
}
}
+114
View File
@@ -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<typeof data[0]>[] = [
{ header: "#", accessor: (row, i) => i + 1 },
{
header: "Application",
accessor: (row) => (
<div className="flex items-center gap-2">
{row.favicon && (
<img src={row.favicon} alt={row.app_label} className="w-5 h-5 rounded-sm bg-white p-0.5" />
)}
<span>{row.app_label}</span>
</div>
)
},
{ header: "Category", accessor: (row) => row.category || "-" },
{
header: "Download",
accessor: (row) => <span className="text-blue-400">{fmtBytes(row.download)}</span>
},
{
header: "Upload",
accessor: (row) => <span className="text-green-500">{fmtBytes(row.upload)}</span>
},
{
header: "Total",
accessor: (row) => <span className="font-medium text-white">{fmtBytes(row.download + row.upload)}</span>
}
];
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold tracking-tight text-white flex items-center gap-3">
Applications
{isLoading && <Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />}
</h2>
</div>
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
<CardHeader>
<CardTitle className="text-white">Top 5 Applications by Volume</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[250px] w-full">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={top5} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#30363d" vertical={false} />
<XAxis dataKey="app_label" stroke="#8b949e" tickLine={false} axisLine={false} />
<YAxis
stroke="#8b949e"
tickLine={false}
axisLine={false}
tickFormatter={(value) => {
const gb = value / 1073741824;
const mb = value / 1048576;
return gb >= 1 ? `${gb.toFixed(1)}GB` : `${mb.toFixed(0)}MB`;
}}
/>
<RechartsTooltip
cursor={{fill: '#21262d'}}
contentStyle={{ backgroundColor: '#161b22', borderColor: '#30363d', color: '#c9d1d9' }}
formatter={(value: any) => [fmtBytes(value as number), 'Volume']}
/>
<Bar dataKey="download" name="Download" fill="#58a6ff" radius={[4, 4, 0, 0]} />
<Bar dataKey="upload" name="Upload" fill="#3fb950" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
<CardHeader>
<CardTitle className="text-white">All Applications ({data.length})</CardTitle>
</CardHeader>
<CardContent>
<DataTable
data={data}
columns={columns}
searchPlaceholder="Search by application or category..."
searchFilter={(row, query) => {
const q = query.toLowerCase();
return (
(row.app_label || "").toLowerCase().includes(q) ||
(row.category || "").toLowerCase().includes(q)
);
}}
/>
</CardContent>
</Card>
</div>
);
}
+64
View File
@@ -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<DeviceStat>[] = [
{ header: "#", accessor: (row, i) => i + 1 },
{ header: "IP Address", accessor: (row) => <span className="font-medium text-blue-400">{row.ip_address || '-'}</span> },
{ header: "MAC Address", accessor: (row) => row.mac_address ? <code className="text-xs bg-secondary/50 px-1 py-0.5 rounded text-muted-foreground">{row.mac_address}</code> : '-' },
{ header: "Type", accessor: (row) => row.device_type || '-' },
{ header: "OS", accessor: (row) => row.os_label || '-' },
{ header: "Manufacturer", accessor: (row) => row.manufacturer || '-' },
{ header: "Download", accessor: (row) => <span className="text-cyan-400">{fmtBytes(row.download)}</span>, className: "text-right" },
{ header: "Upload", accessor: (row) => <span className="text-green-400">{fmtBytes(row.upload)}</span>, className: "text-right" },
];
return (
<div className="space-y-8 animate-fade-in pb-10">
<div className="flex items-center justify-between">
<div>
<h2 className="text-3xl font-bold tracking-tight text-white flex items-center gap-3">
Devices
{devices.isLoading && <Loader2 className="w-5 h-5 animate-spin text-primary" />}
</h2>
<p className="text-muted-foreground mt-1">Monitor and manage connected network devices.</p>
</div>
</div>
<Card className="bg-card/80 backdrop-blur-xl border-border/50 animate-slide-up" style={{ animationDelay: '100ms' }}>
<CardHeader className="border-b border-border/50 pb-4 mb-4">
<CardTitle className="text-lg text-white">Active Devices {devices.data ? `(${devices.data.length})` : ''}</CardTitle>
</CardHeader>
<CardContent>
<DataTable
data={devices.data || []}
columns={columns}
searchPlaceholder="Search devices by IP, MAC, OS..."
searchFilter={(row, query) => {
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);
}}
/>
</CardContent>
</Card>
</div>
);
}
+64
View File
@@ -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<typeof data[0]>[] = [
{ header: "#", accessor: (row, i) => i + 1 },
{
header: "Domain",
accessor: (row) => <span className="font-medium text-white">{row.domain}</span>
},
{ header: "Application", accessor: (row) => row.app_label || "-" },
{ header: "Category", accessor: (row) => row.category || "-" },
{
header: "Query Count",
accessor: (row) => <span className="text-purple-400">{row.query_count.toLocaleString()}</span>
}
];
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold tracking-tight text-white flex items-center gap-3">
DNS Intelligence
{isLoading && <Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />}
</h2>
</div>
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
<CardHeader>
<CardTitle className="text-white">Top DNS Queries ({data.length})</CardTitle>
</CardHeader>
<CardContent>
<DataTable
data={data}
columns={columns}
searchPlaceholder="Search by domain, application, or category..."
searchFilter={(row, query) => {
const q = query.toLowerCase();
return (
(row.domain || "").toLowerCase().includes(q) ||
(row.app_label || "").toLowerCase().includes(q) ||
(row.category || "").toLowerCase().includes(q)
);
}}
/>
</CardContent>
</Card>
</div>
);
}
+217
View File
@@ -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 (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold text-white tracking-tight">DPI Analytics</h1>
<p className="text-slate-400 mt-2">Deep Packet Inspection metadata and fingerprinting.</p>
</div>
<Tabs>
<Tab label="Hostnames & Servers">
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>SNI Hostnames (TLS)</CardTitle>
</CardHeader>
<CardContent>
<DataTable
data={sniHostnames}
isLoading={loadingSni}
columns={[
{ header: "Hostname", accessor: (row: any) => row.sni_hostname },
{ header: "Total", accessor: (row) => formatBytes(row.total) },
]}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>SSL Server Common Names</CardTitle>
</CardHeader>
<CardContent>
<DataTable
data={sslServerCn}
isLoading={loadingSslCn}
columns={[
{ header: "Common Name", accessor: (row: any) => row.ssl_server_cn },
{ header: "Total", accessor: (row) => formatBytes(row.total) },
]}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>QUIC Hostnames</CardTitle>
</CardHeader>
<CardContent>
<DataTable
data={quicHostnames}
isLoading={loadingQuic}
columns={[
{ header: "Hostname", accessor: (row: any) => row.quic_hostname },
{ header: "Total", accessor: (row) => formatBytes(row.total) },
]}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>NetBIOS Hostnames</CardTitle>
</CardHeader>
<CardContent>
<DataTable
data={netbios}
isLoading={loadingNetbios}
columns={[
{ header: "Hostname", accessor: (row: any) => row.hostname },
{ header: "Total", accessor: (row) => formatBytes(row.total) },
]}
/>
</CardContent>
</Card>
<Card className="xl:col-span-2">
<CardHeader>
<CardTitle>mDNS Hostnames</CardTitle>
</CardHeader>
<CardContent>
<DataTable
data={mdnsHostnames}
isLoading={loadingMdns}
columns={[
{ header: "Hostname", accessor: (row: any) => row.mdns_hostname },
{ header: "Total", accessor: (row) => formatBytes(row.total) },
]}
/>
</CardContent>
</Card>
</div>
</Tab>
<Tab label="Client Fingerprints">
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
<Card className="xl:col-span-2">
<CardHeader>
<CardTitle>Operating Systems Discovery</CardTitle>
</CardHeader>
<CardContent>
<DataTable
data={discoveryOs}
isLoading={loadingOs}
columns={[
{ header: "Operating System", accessor: (row: any) => row.os_label },
{ header: "Download", accessor: (row) => formatBytes(row.download) },
{ header: "Upload", accessor: (row) => formatBytes(row.upload) },
{ header: "Total", accessor: (row) => formatBytes(row.total) },
]}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>DHCP Fingerprints</CardTitle>
</CardHeader>
<CardContent>
<DataTable
data={dhcpFingerprints}
isLoading={loadingDhcp}
columns={[
{ header: "Fingerprint", accessor: (row: any) => row.fingerprint },
{ header: "Total", accessor: (row) => formatBytes(row.total) },
]}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>HTTP User Agents</CardTitle>
</CardHeader>
<CardContent>
<DataTable
data={httpUserAgents}
isLoading={loadingHttpUa}
columns={[
{ header: "User Agent", accessor: (row: any) => row.user_agent },
{ header: "Total", accessor: (row) => formatBytes(row.total) },
]}
/>
</CardContent>
</Card>
</div>
</Tab>
<Tab label="Specific Protocols">
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>SSH Versions</CardTitle>
</CardHeader>
<CardContent>
<DataTable
data={sshVersions}
isLoading={loadingSsh}
columns={[
{ header: "SSH Version", accessor: (row: any) => row.ssh_version },
{ header: "Total", accessor: (row) => formatBytes(row.total) },
]}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>BitTorrent Hashes</CardTitle>
</CardHeader>
<CardContent>
<DataTable
data={bittorrentHashes}
isLoading={loadingBittorrent}
columns={[
{ header: "Label", accessor: (row: any) => row.label },
{ header: "Info Hash", accessor: (row: any) => row.info_hash },
{ header: "Total", accessor: (row) => formatBytes(row.total) },
]}
/>
</CardContent>
</Card>
</div>
</Tab>
</Tabs>
</div>
);
}
+93
View File
@@ -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 <AlertCircle className="w-4 h-4 text-red-500" />;
case 'medium':
case 'warning':
return <AlertTriangle className="w-4 h-4 text-yellow-500" />;
default:
return <Info className="w-4 h-4 text-blue-500" />;
}
};
const columns: Column<typeof data[0]>[] = [
{ 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) => (
<div className="flex items-center gap-2">
{getSeverityIcon(row.severity)}
<span className="capitalize">{row.severity}</span>
</div>
)
},
{ header: "Event Type", accessor: (row) => row.event_type },
{
header: "Source IP",
accessor: (row) => <span className="text-blue-400">{row.source_ip || "-"}</span>
},
{ header: "Message", accessor: (row) => row.message }
];
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold tracking-tight text-white flex items-center gap-3">
Network Events
{isLoading && <Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />}
</h2>
</div>
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
<CardHeader>
<CardTitle className="text-white">Recent Events ({data.length})</CardTitle>
</CardHeader>
<CardContent>
<DataTable
data={data}
columns={columns}
searchPlaceholder="Search by event type, severity, IP, or message..."
searchFilter={(row, query) => {
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)
);
}}
/>
</CardContent>
</Card>
</div>
);
}
+71
View File
@@ -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<typeof data[0]>[] = [
{ header: "#", accessor: (row, i) => i + 1 },
{ header: "Flow ID", accessor: (row) => row.flow_id },
{ header: "Src IP", accessor: (row) => <span className="text-blue-400">{row.src_ip}</span> },
{ header: "Dst IP", accessor: (row) => <span className="text-blue-400">{row.dst_ip}</span> },
{ 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) => <span className="text-blue-400">{fmtBytes(row.bytes_download)}</span>
},
{
header: "Upload",
accessor: (row) => <span className="text-green-500">{fmtBytes(row.bytes_upload)}</span>
}
];
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold tracking-tight text-white flex items-center gap-3">
Active Flows
{isLoading && <Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />}
</h2>
</div>
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
<CardHeader>
<CardTitle className="text-white">Active Flows ({data.length})</CardTitle>
</CardHeader>
<CardContent>
<DataTable
data={data}
columns={columns}
searchPlaceholder="Search flows by IP, App, or Domain..."
searchFilter={(row, query) => {
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)
);
}}
/>
</CardContent>
</Card>
</div>
);
}
+101
View File
@@ -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<typeof data[0]>[] = [
{ header: "#", accessor: (row, i) => i + 1 },
{
header: "Country",
accessor: (row) => (
<div className="flex items-center gap-2">
{row.country_code && (
<img
src={`https://flagcdn.com/w20/${row.country_code.toLowerCase()}.png`}
alt={row.country_code}
className="w-5 h-auto rounded-sm"
onError={(e) => {
e.currentTarget.style.display = 'none';
}}
/>
)}
<span>{row.country_name}</span>
</div>
)
},
{ header: "Country Code", accessor: (row) => row.country_code },
{
header: "Download",
accessor: (row) => <span className="text-blue-400">{fmtBytes(row.download)}</span>
},
{
header: "Upload",
accessor: (row) => <span className="text-green-500">{fmtBytes(row.upload)}</span>
},
{
header: "Total Data",
accessor: (row) => <span className="font-medium text-white">{fmtBytes(row.download + row.upload)}</span>
}
];
return (
<div className="space-y-8 animate-fade-in pb-10">
<div className="flex items-center justify-between">
<div>
<h2 className="text-3xl font-bold tracking-tight text-white flex items-center gap-3">
Geographic Traffic
{isLoading && <Loader2 className="w-5 h-5 animate-spin text-primary" />}
</h2>
<p className="text-muted-foreground mt-1">Visualize data flow and activity across the globe.</p>
</div>
</div>
<Card className="bg-card/80 backdrop-blur-xl border-border/50 animate-slide-up" style={{ animationDelay: '100ms' }}>
<CardContent className="p-1 sm:p-4">
<WorldMap data={mapData} />
</CardContent>
</Card>
<Card className="bg-card/80 backdrop-blur-xl border-border/50 animate-slide-up" style={{ animationDelay: '200ms' }}>
<CardHeader className="border-b border-border/50 pb-4 mb-4">
<CardTitle className="text-lg text-white">Traffic by Country ({data.length})</CardTitle>
</CardHeader>
<CardContent>
<DataTable
data={data}
columns={columns}
searchPlaceholder="Search by country name..."
searchFilter={(row, query) => {
const q = query.toLowerCase();
return (row.country_name || "").toLowerCase().includes(q);
}}
/>
</CardContent>
</Card>
</div>
);
}
+65 -14
View File
@@ -1,26 +1,77 @@
@import "tailwindcss"; @import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline { @theme inline {
--color-background: var(--background); --color-background: var(--background);
--color-foreground: var(--foreground); --color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) { --color-card: var(--card);
:root { --color-card-foreground: var(--card-foreground);
--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 { body {
background: var(--background); background-color: var(--background);
color: var(--foreground); 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;
} }
+252
View File
@@ -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<string>("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: <Bitcoin className="h-5 w-5 text-yellow-500" />,
description: "Devices actively mining cryptocurrency"
},
{
title: "Tor Detection",
value: stats?.intel_tor_detection || 0,
icon: <Globe className="h-5 w-5 text-purple-500" />,
description: "Connections using the Tor network"
},
{
title: "VPN Detection",
value: stats?.intel_vpn_detection || 0,
icon: <Shield className="h-5 w-5 text-blue-500" />,
description: "Active Virtual Private Network tunnels"
},
{
title: "IP Reputation",
value: stats?.intel_ip_reputation || 0,
icon: <ShieldAlert className="h-5 w-5 text-red-500" />,
description: "Connections to malicious IP addresses"
},
{
title: "Insecure Protocols",
value: stats?.intel_insecure_protocols || 0,
icon: <AlertTriangle className="h-5 w-5 text-orange-500" />,
description: "Legacy clear-text protocols"
},
{
title: "Unencrypted Passwords",
value: stats?.intel_unencrypted_passwords || 0,
icon: <Key className="h-5 w-5 text-red-400" />,
description: "Credentials transmitted in plain text"
},
{
title: "Encryption Audit",
value: stats?.intel_encryption_audit || 0,
icon: <Lock className="h-5 w-5 text-green-500" />,
description: "Encrypted connections audited"
},
{
title: "Device Discovery",
value: stats?.intel_device_discovery || 0,
icon: <Network className="h-5 w-5 text-cyan-500" />,
description: "Unique client devices identified"
},
{
title: "Server Discovery",
value: stats?.intel_server_discovery || 0,
icon: <Server className="h-5 w-5 text-gray-400" />,
description: "Local servers running services"
}
];
const renderActiveTable = () => {
switch(activeTab) {
case "Crypto Mining":
return (
<DataTable
data={cryptoMining} isLoading={loadingCrypto}
columns={[
{ header: "IP Address", accessor: (row: any) => 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 (
<DataTable
data={torDetection} isLoading={loadingTor}
columns={[
{ header: "IP Address", accessor: (row: any) => row.ip_address },
{ header: "Exit Node", accessor: (row: any) => row.exit_node },
{ header: "Country", accessor: (row: any) => row.country },
]}
/>
);
case "VPN Detection":
return (
<DataTable
data={vpnDetection} isLoading={loadingVpn}
columns={[
{ header: "IP Address", accessor: (row: any) => 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 (
<DataTable
data={ipReputation} isLoading={loadingIpRep}
columns={[
{ header: "IP Address", accessor: (row: any) => 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 (
<DataTable
data={insecureProtocols} isLoading={loadingInsecure}
columns={[
{ header: "Protocol", accessor: (row: any) => row.protocol },
{ header: "IP Address", accessor: (row: any) => row.ip_address },
{ header: "Risk", accessor: (row: any) => row.risk },
]}
/>
);
case "Unencrypted Passwords":
return (
<DataTable
data={unencryptedPasswords} isLoading={loadingPasswords}
columns={[
{ header: "Protocol", accessor: (row: any) => row.protocol },
{ header: "IP Address", accessor: (row: any) => row.ip_address },
{ header: "Severity", accessor: (row: any) => row.severity },
]}
/>
);
case "Encryption Audit":
return (
<DataTable
data={encryptionAudit} isLoading={loadingEncryption}
columns={[
{ header: "IP Address", accessor: (row: any) => row.ip_address },
{ header: "Encrypted %", accessor: (row) => `${row.encrypted_pct}%` },
{ header: "Risk Level", accessor: (row: any) => row.risk_level },
]}
/>
);
case "Device Discovery":
return (
<DataTable
data={deviceDiscovery} isLoading={loadingDevice}
columns={[
{ header: "IP Address", accessor: (row: any) => 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 (
<DataTable
data={serverDiscovery} isLoading={loadingServer}
columns={[
{ header: "IP Address", accessor: (row: any) => 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 (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold tracking-tight text-white flex items-center gap-3">
Deep Packet Inspection Intelligence
{loadingStats && <Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />}
</h2>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{intelItems.map((item, idx) => {
const isActive = activeTab === item.title;
return (
<Card
key={idx}
onClick={() => 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' : ''}
`}
>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-white">{item.title}</CardTitle>
{item.icon}
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-white mb-1">
{item.value.toLocaleString()}
</div>
<p className="text-xs text-muted-foreground">
{item.description}
</p>
</CardContent>
</Card>
);
})}
</div>
<Card>
<CardHeader>
<CardTitle>Detailed Report: {activeTab}</CardTitle>
</CardHeader>
<CardContent>
{renderActiveTable()}
</CardContent>
</Card>
</div>
);
}
+10 -12
View File
@@ -1,20 +1,16 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Inter } from "next/font/google";
import "./globals.css"; import "./globals.css";
import { DashboardLayout } from "@/components/layout/DashboardLayout";
const geistSans = Geist({ const inter = Inter({
variable: "--font-geist-sans", variable: "--font-inter",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"], subsets: ["latin"],
}); });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Create Next App", title: "Netify Dashboard",
description: "Generated by create next app", description: "Netify Network Intelligence Dashboard",
}; };
export default function RootLayout({ export default function RootLayout({
@@ -25,9 +21,11 @@ export default function RootLayout({
return ( return (
<html <html
lang="en" lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`} className={`${inter.variable} h-full antialiased dark`}
> >
<body className="min-h-full flex flex-col">{children}</body> <body className="min-h-full flex flex-col">
<DashboardLayout>{children}</DashboardLayout>
</body>
</html> </html>
); );
} }
+189
View File
@@ -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 (
<div className="space-y-6 animate-fade-in pb-10">
<div>
<h1 className="text-3xl font-bold text-white tracking-tight flex items-center gap-3">
Application Database
{isLoading && <Loader2 className="w-5 h-5 animate-spin text-primary" />}
</h1>
<p className="text-slate-400 mt-2">
Search Netify's extensive classification database of over 2,500+ apps, protocols, and services.
</p>
</div>
{/* Search Input Bar */}
<div className="relative max-w-md w-full animate-slide-up" style={{ animationDelay: '50ms' }}>
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-slate-400" />
<input
type="text"
placeholder="Search by app name or tag (e.g. facebook, openvpn)..."
className="w-full bg-card/60 backdrop-blur-xl border border-border/60 rounded-xl pl-10 pr-4 py-3 text-white placeholder-slate-400 focus:outline-none focus:border-primary/80 focus:ring-1 focus:ring-primary/80 transition-all duration-200"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
{/* Cards Grid */}
{error ? (
<div className="p-8 bg-red-950/20 border border-red-500/30 rounded-xl text-center text-red-400">
<p className="font-semibold">Failed to load application lookup database</p>
<p className="text-sm mt-1">{error.message}</p>
</div>
) : applications.length === 0 && !isLoading ? (
<div className="p-16 bg-card/40 border border-border/50 rounded-2xl text-center text-slate-400 max-w-lg mx-auto">
<AppWindow className="h-12 w-12 mx-auto mb-4 text-slate-500" />
<p className="font-semibold text-white">No applications found</p>
<p className="text-sm mt-1">Try refining your search keyword or clearing the input.</p>
</div>
) : (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 animate-slide-up" style={{ animationDelay: '100ms' }}>
{applications.map((app: LookupApp) => (
<Card
key={app.id}
className="group relative overflow-hidden bg-card/60 backdrop-blur-xl border-border/50 hover:bg-card/100 hover:border-primary/40 transition-all duration-300 flex flex-col justify-between"
>
<div className="p-5 flex flex-col h-full justify-between gap-4">
{/* Top: Icon + Title */}
<div className="space-y-3">
<div className="flex items-start justify-between">
<div className="h-10 w-10 rounded-lg bg-secondary/80 flex items-center justify-center overflow-hidden border border-border/40 group-hover:border-primary/20 transition-all">
{app.favicon || app.logo ? (
<img
src={app.favicon || app.logo}
alt={app.label}
className="h-6 w-6 object-contain"
onError={(e) => {
// If image fails, replace with default icon
(e.target as HTMLElement).style.display = 'none';
}}
/>
) : (
<AppWindow className="h-5 w-5 text-slate-400" />
)}
</div>
<span className="text-[10px] uppercase font-semibold tracking-wider text-primary bg-primary/10 border border-primary/20 px-2 py-0.5 rounded-full">
{app.application_category?.label || "Unclassified"}
</span>
</div>
<div>
<h3 className="font-bold text-white group-hover:text-primary transition-colors text-base line-clamp-1">
{app.label}
</h3>
<p className="text-[11px] text-slate-500 font-mono mt-0.5">{app.tag}</p>
</div>
<p className="text-xs text-slate-400 line-clamp-3 leading-relaxed mt-2">
{app.description || "No description provided for this application definition."}
</p>
</div>
{/* Bottom: ID and system information */}
<div className="flex items-center justify-between pt-2 border-t border-border/30 text-[10px] text-slate-500 font-mono">
<span>ID: {app.id}</span>
<span>{app.name || "N/A"}</span>
</div>
</div>
</Card>
))}
</div>
{/* Pagination Controls */}
{pagination.total_pages > 1 && (
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 pt-4 border-t border-border/50 text-slate-400 text-sm animate-slide-up" style={{ animationDelay: '150ms' }}>
<div>
Showing <span className="text-white font-medium">{((page - 1) * limit) + 1}</span> to{" "}
<span className="text-white font-medium">
{Math.min(page * limit, pagination.total_records)}
</span>{" "}
of <span className="text-white font-medium">{pagination.total_records}</span> entries
</div>
<div className="flex items-center gap-1">
{/* First Page */}
<button
onClick={() => setPage(1)}
disabled={page === 1 || isLoading}
className="p-2 rounded-lg bg-card border border-border/50 text-slate-400 hover:bg-secondary disabled:opacity-30 disabled:hover:bg-card transition-colors"
title="First Page"
>
<ChevronsLeft className="h-4 w-4" />
</button>
{/* Prev Page */}
<button
onClick={() => setPage(p => Math.max(p - 1, 1))}
disabled={page === 1 || isLoading}
className="p-2 rounded-lg bg-card border border-border/50 text-slate-400 hover:bg-secondary disabled:opacity-30 disabled:hover:bg-card transition-colors"
title="Previous Page"
>
<ChevronLeft className="h-4 w-4" />
</button>
{/* Page indicators */}
<span className="px-4 py-1.5 rounded-lg bg-secondary/50 border border-border/30 text-white font-mono">
{page} / {pagination.total_pages}
</span>
{/* Next Page */}
<button
onClick={() => setPage(p => Math.min(p + 1, pagination.total_pages))}
disabled={page === pagination.total_pages || isLoading}
className="p-2 rounded-lg bg-card border border-border/50 text-slate-400 hover:bg-secondary disabled:opacity-30 disabled:hover:bg-card transition-colors"
title="Next Page"
>
<ChevronRight className="h-4 w-4" />
</button>
{/* Last Page */}
<button
onClick={() => setPage(pagination.total_pages)}
disabled={page === pagination.total_pages || isLoading}
className="p-2 rounded-lg bg-card border border-border/50 text-slate-400 hover:bg-secondary disabled:opacity-30 disabled:hover:bg-card transition-colors"
title="Last Page"
>
<ChevronsRight className="h-4 w-4" />
</button>
</div>
</div>
)}
</div>
)}
</div>
);
}
+225
View File
@@ -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 (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold text-white tracking-tight">Network Infrastructure</h1>
<p className="text-slate-400 mt-2">Physical, logical, and geographic network topologies.</p>
</div>
<Tabs>
<Tab label="Routing & Geography">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>Top Regions</CardTitle>
</CardHeader>
<CardContent>
<DataTable
data={regions}
isLoading={loadingRegions}
columns={[
{ header: "Region", accessor: (row: any) => row.region_name },
{ header: "Country", accessor: (row: any) => row.country_name },
{ header: "Download", accessor: (row) => formatBytes(row.download) },
]}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Top Cities</CardTitle>
</CardHeader>
<CardContent>
<DataTable
data={cities}
isLoading={loadingCities}
columns={[
{ header: "City", accessor: (row: any) => 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) },
]}
/>
</CardContent>
</Card>
</div>
</Tab>
<Tab label="Interfaces & VLANs">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>Network Interfaces</CardTitle>
</CardHeader>
<CardContent>
<DataTable
data={interfaces}
isLoading={loadingInterfaces}
columns={[
{ header: "Interface", accessor: (row: any) => 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) },
]}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>VLANs</CardTitle>
</CardHeader>
<CardContent>
<DataTable
data={vlans}
isLoading={loadingVlans}
columns={[
{ header: "VLAN ID", accessor: (row: any) => 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) },
]}
/>
</CardContent>
</Card>
</div>
</Tab>
<Tab label="IP & MAC Properties">
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
<div className="xl:col-span-2">
<Card>
<CardHeader>
<CardTitle>Top Remote IPs</CardTitle>
</CardHeader>
<CardContent>
<DataTable
data={remoteIps}
isLoading={loadingRemoteIps}
columns={[
{ header: "IP Address", accessor: (row: any) => 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) },
]}
/>
</CardContent>
</Card>
</div>
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>IP Versions</CardTitle>
</CardHeader>
<CardContent>
<DataTable
data={ipVersions}
isLoading={loadingIpVersions}
columns={[
{ header: "Version", accessor: (row: any) => row.ip_version_label },
{ header: "Total Traffic", accessor: (row) => formatBytes(row.total) },
]}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>MAC Bandwidth</CardTitle>
</CardHeader>
<CardContent>
<DataTable
data={macBandwidth}
isLoading={loadingMacBandwidth}
columns={[
{ header: "MAC Address", accessor: (row: any) => row.mac_address },
{ header: "Manufacturer", accessor: (row: any) => row.manufacturer },
{ header: "Total", accessor: (row) => formatBytes(row.total) },
]}
/>
</CardContent>
</Card>
</div>
</div>
</Tab>
<Tab label="Flow Profiles">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>Flow Types</CardTitle>
</CardHeader>
<CardContent>
<DataTable
data={flowTypes}
isLoading={loadingFlowTypes}
columns={[
{ header: "Flow Type", accessor: (row: any) => 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) },
]}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Flow Origins</CardTitle>
</CardHeader>
<CardContent>
<DataTable
data={flowOrigins}
isLoading={loadingFlowOrigins}
columns={[
{ header: "Origin", accessor: (row: any) => 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) },
]}
/>
</CardContent>
</Card>
</div>
</Tab>
</Tabs>
</div>
);
}
+119
View File
@@ -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<AppCategoryStat>[] = [
{ header: "#", accessor: (row, i) => i + 1 },
{ header: "Category", accessor: (row) => <span className="font-semibold text-white">{row.category_label || '-'}</span> },
{ header: "Download", accessor: (row) => <span className="text-cyan-400">{fmtBytes(row.download)}</span> },
{ header: "Upload", accessor: (row) => <span className="text-green-400">{fmtBytes(row.upload)}</span> },
{ header: "Total", accessor: (row) => fmtBytes(row.total) },
];
const isLoading = appCategories.isLoading || continents.isLoading;
return (
<div className="space-y-8 animate-fade-in pb-10">
<div className="flex items-center justify-between">
<div>
<h2 className="text-3xl font-bold tracking-tight text-white flex items-center gap-3">
Network Intelligence
{isLoading && <Loader2 className="w-5 h-5 animate-spin text-primary" />}
</h2>
<p className="text-muted-foreground mt-1">Deep packet inspection analytics and geography.</p>
</div>
<div className="flex items-center gap-2 text-sm font-medium text-blue-400 bg-blue-500/10 border border-blue-500/20 px-4 py-2 rounded-lg shadow-[0_0_15px_rgba(59,130,246,0.1)]">
<Activity className="h-4 w-4" />
<span>DPI Engine Active</span>
</div>
</div>
<div className="grid gap-6 md:grid-cols-2">
{/* App Categories */}
<Card className="bg-card/80 backdrop-blur-xl border-border/50 animate-slide-up" style={{ animationDelay: '100ms' }}>
<CardHeader className="border-b border-border/50 pb-4 mb-4">
<CardTitle className="text-lg text-white">App Categories</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="h-[200px]">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={appCategories.data || []}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={80}
paddingAngle={2}
dataKey="download"
nameKey="category_label"
>
{(appCategories.data || []).map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<RechartsTooltip
contentStyle={{ backgroundColor: '#161b22', borderColor: '#30363d', color: '#c9d1d9' }}
formatter={(val: any) => fmtBytes(val as number)}
/>
</PieChart>
</ResponsiveContainer>
</div>
<DataTable data={appCategories.data || []} columns={appCols} />
</CardContent>
</Card>
{/* Continents */}
<Card className="bg-card/80 backdrop-blur-xl border-border/50 animate-slide-up" style={{ animationDelay: '200ms' }}>
<CardHeader className="border-b border-border/50 pb-4 mb-4">
<CardTitle className="text-lg text-white">Continents (Download Volume)</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="h-[200px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={continents.data || []} layout="vertical" margin={{ top: 0, right: 30, left: 40, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#30363d" horizontal={true} vertical={false} />
<XAxis type="number" stroke="#8b949e" tickFormatter={(v) => `${(v/1000000000).toFixed(1)}GB`} />
<YAxis dataKey="continent_name" type="category" stroke="#8b949e" fontSize={12} />
<RechartsTooltip
cursor={{fill: '#21262d'}}
contentStyle={{ backgroundColor: '#161b22', borderColor: '#30363d', color: '#c9d1d9' }}
formatter={(val: any) => fmtBytes(val as number)}
/>
<Bar dataKey="download" fill="#58a6ff" radius={[0, 4, 4, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
<div className="mt-4 border border-border rounded-md p-4 bg-secondary/30">
<p className="text-sm text-muted-foreground leading-relaxed">
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.
</p>
</div>
</CardContent>
</Card>
</div>
</div>
);
}
+295 -58
View File
@@ -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 ( return (
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black"> <div className="space-y-8 animate-fade-in pb-10">
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start"> <div className="flex items-center justify-between">
<Image <div>
className="dark:invert" <h2 className="text-3xl font-bold tracking-tight text-white flex items-center gap-3">
src="/next.svg" Summary Overview
alt="Next.js logo" {isLoading && <Loader2 className="w-5 h-5 animate-spin text-primary" />}
width={100} </h2>
height={20} <p className="text-muted-foreground mt-1">Real-time network traffic and intelligence summary.</p>
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
</div> </div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row"> </div>
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]" {/* KPI Cards */}
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
target="_blank" <Card className="group relative overflow-hidden bg-card/80 backdrop-blur-xl border-border/50 hover:bg-card/100 hover:border-primary/30 transition-all duration-300 cursor-default animate-slide-up" style={{ animationDelay: '0ms' }}>
rel="noopener noreferrer" <div className="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity">
> <Download className="h-16 w-16 text-blue-500" />
<Image </div>
className="dark:invert" <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2 relative z-10">
src="/vercel.svg" <CardTitle className="text-sm font-medium text-muted-foreground group-hover:text-blue-400 transition-colors">Download</CardTitle>
alt="Vercel logomark" <Download className="h-4 w-4 text-blue-500" />
width={16} </CardHeader>
height={16} <CardContent className="relative z-10">
/> <div className="text-3xl font-bold text-white tracking-tight">
Deploy Now {summary.data ? fmtBytes(summary.data.bandwidth_down) : "0 B"}
</a> </div>
<a </CardContent>
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]" </Card>
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" <Card className="group relative overflow-hidden bg-card/80 backdrop-blur-xl border-border/50 hover:bg-card/100 hover:border-emerald-500/30 transition-all duration-300 cursor-default animate-slide-up" style={{ animationDelay: '50ms' }}>
target="_blank" <div className="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity">
rel="noopener noreferrer" <Upload className="h-16 w-16 text-emerald-500" />
> </div>
Documentation <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2 relative z-10">
</a> <CardTitle className="text-sm font-medium text-muted-foreground group-hover:text-emerald-400 transition-colors">Upload</CardTitle>
<Upload className="h-4 w-4 text-emerald-500" />
</CardHeader>
<CardContent className="relative z-10">
<div className="text-3xl font-bold text-white tracking-tight">
{summary.data ? fmtBytes(summary.data.bandwidth_up) : "0 B"}
</div>
</CardContent>
</Card>
<Card className="group relative overflow-hidden bg-card/80 backdrop-blur-xl border-border/50 hover:bg-card/100 hover:border-orange-500/30 transition-all duration-300 cursor-default animate-slide-up" style={{ animationDelay: '100ms' }}>
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity">
<Server className="h-16 w-16 text-orange-500" />
</div>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2 relative z-10">
<CardTitle className="text-sm font-medium text-muted-foreground group-hover:text-orange-400 transition-colors">Devices</CardTitle>
<Server className="h-4 w-4 text-orange-500" />
</CardHeader>
<CardContent className="relative z-10">
<div className="text-3xl font-bold text-white tracking-tight">
{summary.data?.total_devices?.toLocaleString() || "0"}
</div>
</CardContent>
</Card>
<Card className="group relative overflow-hidden bg-card/80 backdrop-blur-xl border-border/50 hover:bg-card/100 hover:border-purple-500/30 transition-all duration-300 cursor-default animate-slide-up" style={{ animationDelay: '150ms' }}>
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity">
<Activity className="h-16 w-16 text-purple-500" />
</div>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2 relative z-10">
<CardTitle className="text-sm font-medium text-muted-foreground group-hover:text-purple-400 transition-colors">Flows</CardTitle>
<Activity className="h-4 w-4 text-purple-500" />
</CardHeader>
<CardContent className="relative z-10">
<div className="text-3xl font-bold text-white tracking-tight">
{summary.data?.active_flows?.toLocaleString() || "0"}
</div>
</CardContent>
</Card>
<Card className="group relative overflow-hidden bg-card/80 backdrop-blur-xl border-red-500/20 hover:bg-card/100 hover:border-red-500/50 transition-all duration-300 cursor-default animate-slide-up" style={{ animationDelay: '200ms' }}>
<div className="absolute inset-0 bg-red-500/5 pointer-events-none" />
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity text-red-500">
<ShieldAlert className="h-16 w-16" />
</div>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2 relative z-10">
<CardTitle className="text-sm font-medium text-red-400 group-hover:text-red-300 transition-colors">Threats</CardTitle>
<ShieldAlert className="h-4 w-4 text-red-500 animate-pulse" />
</CardHeader>
<CardContent className="relative z-10">
<div className="text-3xl font-bold text-red-500 tracking-tight">
{summary.data?.total_threats?.toLocaleString() || "0"}
</div>
</CardContent>
</Card>
<Card className="group relative overflow-hidden bg-card/80 backdrop-blur-xl border-border/50 hover:bg-card/100 hover:border-yellow-500/30 transition-all duration-300 cursor-default animate-slide-up" style={{ animationDelay: '250ms' }}>
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity text-yellow-500">
<Zap className="h-16 w-16" />
</div>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2 relative z-10">
<CardTitle className="text-sm font-medium text-muted-foreground group-hover:text-yellow-400 transition-colors">Events</CardTitle>
<Zap className="h-4 w-4 text-yellow-500" />
</CardHeader>
<CardContent className="relative z-10">
<div className="text-3xl font-bold text-white tracking-tight">
{summary.data?.total_events?.toLocaleString() || "0"}
</div>
</CardContent>
</Card>
</div>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-7 animate-slide-up" style={{ animationDelay: '300ms' }}>
{/* Timeline Chart */}
<Card className="col-span-4 bg-card/80 backdrop-blur-xl border-border/50">
<CardHeader>
<CardTitle className="text-lg text-white">Bandwidth Timeline</CardTitle>
</CardHeader>
<CardContent className="pl-2">
<div className="h-[350px] w-full">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={timeline.data} margin={{ top: 15, right: 20, left: 10, bottom: 5 }}>
<defs>
<linearGradient id="colorDownload" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.3}/>
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0}/>
</linearGradient>
<linearGradient id="colorUpload" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#10b981" stopOpacity={0.3}/>
<stop offset="95%" stopColor="#10b981" stopOpacity={0}/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" vertical={false} />
<XAxis
dataKey="fetched_at"
stroke="#a1a1aa"
fontSize={12}
tickLine={false}
axisLine={false}
tickMargin={10}
tickFormatter={(val) => {
try {
return new Date(val).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
} catch (e) {
return val;
}
}}
/>
<YAxis
stroke="#a1a1aa"
fontSize={12}
tickLine={false}
axisLine={false}
tickFormatter={(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];
}}
/>
<RechartsTooltip
contentStyle={{
backgroundColor: 'rgba(24, 24, 27, 0.9)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '8px',
color: '#fafafa',
boxShadow: '0 4px 20px rgba(0,0,0,0.3)'
}}
itemStyle={{ color: '#fafafa', fontSize: '13px', fontWeight: 500 }}
labelFormatter={(label) => {
try {
return new Date(label).toLocaleString();
} catch (e) {
return label;
}
}}
formatter={(value: any, name: any) => [fmtBytes(value as number), name === 'total_download' ? 'Download' : 'Upload']}
/>
<Area type="monotone" dataKey="total_download" stroke="#3b82f6" strokeWidth={3} fillOpacity={1} fill="url(#colorDownload)" activeDot={{ r: 6, fill: "#3b82f6", stroke: "#18181b", strokeWidth: 2 }} />
<Area type="monotone" dataKey="total_upload" stroke="#10b981" strokeWidth={3} fillOpacity={1} fill="url(#colorUpload)" activeDot={{ r: 6, fill: "#10b981", stroke: "#18181b", strokeWidth: 2 }} />
</AreaChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
{/* Top Apps & Protocols */}
<div className="col-span-3 space-y-6">
<Card className="bg-card/80 backdrop-blur-xl border-border/50 flex-1">
<CardHeader>
<CardTitle className="text-lg text-white">Top Apps (Download)</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[150px] w-full">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={topApps.data} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" vertical={false} />
<XAxis dataKey="app_label" stroke="#a1a1aa" fontSize={11} tickLine={false} axisLine={false} tickMargin={5} />
<YAxis
stroke="#a1a1aa"
fontSize={11}
tickLine={false}
axisLine={false}
tickFormatter={(value) => {
const mb = value / 1048576;
return `${mb.toFixed(0)}M`;
}}
/>
<RechartsTooltip
cursor={{fill: 'rgba(255,255,255,0.05)'}}
contentStyle={{
backgroundColor: 'rgba(24, 24, 27, 0.9)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '8px',
color: '#fafafa',
}}
itemStyle={{ color: '#fafafa', fontSize: '13px', fontWeight: 500 }}
formatter={(value: any) => [fmtBytes(value as number), 'Download']}
/>
<Bar dataKey="download" fill="#8b5cf6" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
<Card className="bg-card/80 backdrop-blur-xl border-border/50 flex-1">
<CardHeader>
<CardTitle className="text-lg text-white">Top Protocols</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[150px] w-full flex items-center justify-center">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={topProtocols.data}
cx="50%"
cy="50%"
innerRadius={45}
outerRadius={70}
paddingAngle={4}
dataKey="download"
nameKey="protocol_label"
stroke="none"
>
{topProtocols.data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} className="hover:opacity-80 transition-opacity outline-none" />
))}
</Pie>
<RechartsTooltip
contentStyle={{
backgroundColor: 'rgba(24, 24, 27, 0.9)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '8px',
color: '#fafafa'
}}
itemStyle={{ color: '#fafafa', fontSize: '13px', fontWeight: 500 }}
/>
</PieChart>
</ResponsiveContainer>
{/* Custom Legend */}
<div className="flex flex-col justify-center ml-2 space-y-3">
{topProtocols.data.slice(0, 5).map((entry, index) => (
<div key={`${entry.protocol_label}-${index}`} className="flex items-center text-sm text-muted-foreground whitespace-nowrap group cursor-default">
<div className="w-3.5 h-3.5 rounded-full mr-3 shrink-0 shadow-sm" style={{ backgroundColor: COLORS[index % COLORS.length] }} />
<span className="truncate max-w-[100px] group-hover:text-white transition-colors">{entry.protocol_label}</span>
</div>
))}
</div>
</div>
</CardContent>
</Card>
</div> </div>
</main> </div>
</div> </div>
); );
} }
+192
View File
@@ -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<typeof versions.data[0]>[] = [
{ header: "#", accessor: (row, i) => i + 1 },
{ header: "TLS Version", accessor: (row) => row.tls_version },
{ header: "Download", accessor: (row) => <span className="text-blue-400">{fmtBytes(row.download)}</span> },
{ header: "Upload", accessor: (row) => <span className="text-green-500">{fmtBytes(row.upload)}</span> },
{ header: "Total Volume", accessor: (row) => <span className="font-medium text-white">{fmtBytes(row.total)}</span> }
];
const cipherColumns: Column<typeof ciphers.data[0]>[] = [
{ header: "#", accessor: (row, i) => i + 1 },
{ header: "Cipher Suite", accessor: (row) => <span className="text-purple-400">{row.tls_cipher}</span> },
{ header: "Download", accessor: (row) => <span className="text-blue-400">{fmtBytes(row.download)}</span> },
{ header: "Upload", accessor: (row) => <span className="text-green-500">{fmtBytes(row.upload)}</span> },
{ header: "Total Volume", accessor: (row) => <span className="font-medium text-white">{fmtBytes(row.total)}</span> }
];
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold tracking-tight text-white flex items-center gap-3">
Security Audit (TLS)
{isLoading && <Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />}
</h2>
</div>
{/* Security Overview Cards */}
<div className="grid gap-4 md:grid-cols-3">
{security.data.map((item) => (
<Card key={item.tls_security} className="bg-card/50 backdrop-blur-sm border-border/50">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
{item.tls_security} Connections
</CardTitle>
{item.tls_security === "Recommended" && <ShieldCheck className="h-4 w-4 text-green-500" />}
{item.tls_security === "Secure" && <ShieldCheck className="h-4 w-4 text-blue-500" />}
{item.tls_security === "Weak" && <ShieldAlert className="h-4 w-4 text-orange-500" />}
{item.tls_security === "Insecure" && <ShieldX className="h-4 w-4 text-red-500" />}
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-white">
{fmtBytes(item.total)}
</div>
</CardContent>
</Card>
))}
</div>
<div className="grid gap-4 md:grid-cols-2">
{/* TLS Security Pie Chart */}
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
<CardHeader>
<CardTitle className="text-white">Security Level Distribution</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[250px] w-full flex items-center justify-center">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={security.data}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={80}
paddingAngle={2}
dataKey="total"
nameKey="tls_security"
>
{security.data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[entry.tls_security as keyof typeof COLORS] || "#8884d8"} />
))}
</Pie>
<RechartsTooltip
contentStyle={{ backgroundColor: '#161b22', borderColor: '#30363d', color: '#c9d1d9' }}
formatter={(value: any) => [fmtBytes(value as number), 'Volume']}
/>
</PieChart>
</ResponsiveContainer>
<div className="flex flex-col justify-center ml-4 space-y-2">
{security.data.map((entry, index) => (
<div key={`${entry.tls_security}-${index}`} className="flex items-center text-xs text-muted-foreground whitespace-nowrap">
<div
className="w-3 h-3 rounded-full mr-2 shrink-0"
style={{ backgroundColor: COLORS[entry.tls_security as keyof typeof COLORS] || "#8884d8" }}
/>
<span>{entry.tls_security}</span>
</div>
))}
</div>
</div>
</CardContent>
</Card>
{/* TLS Versions Bar Chart */}
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
<CardHeader>
<CardTitle className="text-white">TLS Versions</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[250px] w-full">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={versions.data} margin={{ top: 10, right: 10, left: 20, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#30363d" vertical={false} />
<XAxis dataKey="tls_version" stroke="#8b949e" fontSize={10} tickLine={false} axisLine={false} />
<YAxis
stroke="#8b949e"
fontSize={10}
tickLine={false}
axisLine={false}
tickFormatter={(value) => {
const gb = value / 1073741824;
const mb = value / 1048576;
return gb >= 1 ? `${gb.toFixed(1)}GB` : `${mb.toFixed(0)}MB`;
}}
/>
<RechartsTooltip
cursor={{fill: '#21262d'}}
contentStyle={{ backgroundColor: '#161b22', borderColor: '#30363d', color: '#c9d1d9' }}
formatter={(value: any) => [fmtBytes(value as number), 'Volume']}
/>
<Bar dataKey="total" fill="#bc8cff" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
</div>
<div className="grid gap-4 md:grid-cols-2">
{/* TLS Versions Table */}
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
<CardHeader>
<CardTitle className="text-white">TLS Versions Overview</CardTitle>
</CardHeader>
<CardContent>
<DataTable
data={versions.data}
columns={versionColumns}
searchPlaceholder="Search versions..."
searchFilter={(row, query) => (row.tls_version || "").toLowerCase().includes(query.toLowerCase())}
/>
</CardContent>
</Card>
{/* TLS Ciphers Table */}
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
<CardHeader>
<CardTitle className="text-white">Top TLS Cipher Suites</CardTitle>
</CardHeader>
<CardContent>
<DataTable
data={ciphers.data}
columns={cipherColumns}
searchPlaceholder="Search ciphers..."
searchFilter={(row, query) => (row.tls_cipher || "").toLowerCase().includes(query.toLowerCase())}
/>
</CardContent>
</Card>
</div>
</div>
);
}
+78
View File
@@ -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 <Badge variant="green">Unknown</Badge>;
const s = sev.toLowerCase();
if (s.includes('high') || s.includes('critical')) return <Badge variant="red">{sev}</Badge>;
if (s.includes('med')) return <Badge variant="orange">{sev}</Badge>;
if (s.includes('low')) return <Badge variant="yellow">{sev}</Badge>;
return <Badge variant="green">{sev}</Badge>;
};
const columns: Column<ThreatStat>[] = [
{ 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) => <span className="font-medium text-blue-400">{row.ip_address || '-'}</span> },
{ header: "Dest IP", accessor: (row) => row.dst_ip || '-' },
{ header: "MAC Address", accessor: (row) => row.mac_address ? <code className="text-xs bg-secondary/50 px-1 py-0.5 rounded text-muted-foreground">{row.mac_address}</code> : '-' },
{ header: "App", accessor: (row) => row.app_label || '-' },
{ header: "Domain", accessor: (row) => row.domain || '-' },
{ header: "Time", accessor: (row) => <span className="text-muted-foreground text-xs">{row.detected_at ? new Date(row.detected_at).toLocaleString() : '-'}</span> },
];
return (
<div className="space-y-8 animate-fade-in pb-10">
<div className="flex items-center justify-between">
<div>
<h2 className="text-3xl font-bold tracking-tight text-white flex items-center gap-3">
Threat Intelligence
{threats.isLoading && <Loader2 className="w-5 h-5 animate-spin text-primary" />}
</h2>
<p className="text-muted-foreground mt-1">Monitor and respond to network security threats.</p>
</div>
<div className="flex items-center gap-2 text-sm font-medium text-emerald-400 bg-emerald-500/10 border border-emerald-500/20 px-4 py-2 rounded-lg shadow-[0_0_15px_rgba(16,185,129,0.1)]">
<ShieldCheck className="h-4 w-4" />
<span>Active Monitoring</span>
</div>
</div>
<Card className="bg-card/80 backdrop-blur-xl border-border/50 animate-slide-up" style={{ animationDelay: '100ms' }}>
<CardHeader className="border-b border-border/50 pb-4 mb-4">
<CardTitle className="text-lg text-white">Detected Threats {threats.data ? `(${threats.data.length})` : ''}</CardTitle>
</CardHeader>
<CardContent>
<DataTable
data={threats.data || []}
columns={columns}
searchPlaceholder="Search threats by IP, domain, type..."
searchFilter={(row, query) => {
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);
}}
/>
</CardContent>
</Card>
</div>
);
}
+40
View File
@@ -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<string>("—");
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 (
<div className="flex h-screen overflow-hidden bg-background">
<Sidebar />
<div className="flex flex-1 flex-col overflow-hidden">
<Header
lastUpdate={lastUpdate}
isRefreshing={isRefreshing}
onRefresh={handleRefresh}
/>
<main className="flex-1 overflow-y-auto bg-[#0d1117] p-6">
<div className="mx-auto max-w-7xl space-y-6">
{children}
</div>
</main>
</div>
</div>
);
}
+41
View File
@@ -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 (
<header className="flex h-16 shrink-0 items-center justify-between border-b border-border bg-background px-6">
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 rounded-full border border-border bg-card px-3 py-1 text-xs text-muted-foreground">
<div className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
Site: {process.env.NEXT_PUBLIC_SITE_ID || "Unknown Site"}
</div>
</div>
<div className="flex items-center gap-4">
<div className="text-xs text-muted-foreground">
Update: {lastUpdate}
</div>
<button
onClick={onRefresh}
disabled={isRefreshing}
className="flex items-center gap-2 rounded-md bg-secondary px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-secondary/80 disabled:opacity-50"
>
<RefreshCw className={`h-4 w-4 ${isRefreshing ? 'animate-spin' : ''}`} />
Refresh
</button>
<button className="relative rounded-full p-2 text-muted-foreground hover:bg-secondary hover:text-white transition-colors">
<Bell className="h-5 w-5" />
<span className="absolute right-2 top-2 h-2 w-2 rounded-full bg-destructive" />
</button>
</div>
</header>
);
}
+96
View File
@@ -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 (
<div className="flex h-full w-64 flex-col border-r border-border/50 bg-sidebar/80 backdrop-blur-2xl relative">
{/* Subtle glow effect behind sidebar */}
<div className="absolute inset-0 bg-gradient-to-b from-primary/5 via-transparent to-transparent pointer-events-none" />
<div className="flex h-[72px] shrink-0 items-center px-6 border-b border-border/50 relative z-10">
<div className="relative flex items-center justify-center w-8 h-8 rounded-lg bg-primary/10 border border-primary/20 mr-3 shadow-[0_0_15px_rgba(59,130,246,0.2)]">
<Shield className="h-5 w-5 text-primary" />
</div>
<span className="text-lg font-bold text-white tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-white to-white/70">
Netify
</span>
</div>
<div className="flex flex-1 flex-col overflow-y-auto relative z-10">
<nav className="flex-1 space-y-1.5 px-3 py-6">
{navigation.map((item) => {
const isActive = pathname === item.href;
return (
<Link
key={item.name}
href={item.href}
className={cn(
"group relative flex items-center rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200 overflow-hidden",
isActive
? "bg-primary/10 text-white shadow-[0_0_10px_rgba(59,130,246,0.1)]"
: "text-muted-foreground hover:bg-secondary/50 hover:text-white"
)}
>
{/* Active indicator bar */}
{isActive && (
<div className="absolute left-0 top-0 bottom-0 w-1 bg-primary rounded-r-full shadow-[0_0_10px_rgba(59,130,246,0.8)]" />
)}
<item.icon
className={cn(
"mr-3 h-5 w-5 shrink-0 transition-all duration-300",
isActive
? "text-primary drop-shadow-[0_0_8px_rgba(59,130,246,0.5)] scale-110"
: "text-muted-foreground group-hover:text-white group-hover:scale-110"
)}
aria-hidden="true"
/>
<span className="relative z-10">{item.name}</span>
{/* Hover background effect */}
{!isActive && (
<div className="absolute inset-0 bg-gradient-to-r from-white/0 via-white/5 to-white/0 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none translate-x-[-100%] group-hover:translate-x-[100%] transition-transform" />
)}
</Link>
);
})}
</nav>
</div>
</div>
);
}
+26
View File
@@ -0,0 +1,26 @@
import { cn } from "@/lib/utils";
interface BadgeProps extends React.HTMLAttributes<HTMLDivElement> {
variant?: "default" | "red" | "orange" | "yellow" | "green" | "blue" | "cyan";
}
export function Badge({ className, variant = "default", ...props }: BadgeProps) {
return (
<div
className={cn(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
"border-primary/20 bg-primary/10 text-primary": variant === "default",
"border-red-500/20 bg-red-500/10 text-red-500": variant === "red",
"border-orange-500/20 bg-orange-500/10 text-orange-500": variant === "orange",
"border-yellow-500/20 bg-yellow-500/10 text-yellow-500": variant === "yellow",
"border-emerald-500/20 bg-emerald-500/10 text-emerald-500": variant === "green",
"border-blue-500/20 bg-blue-500/10 text-blue-500": variant === "blue",
"border-cyan-500/20 bg-cyan-500/10 text-cyan-500": variant === "cyan",
},
className
)}
{...props}
/>
);
}
+68
View File
@@ -0,0 +1,68 @@
import { cn } from "@/lib/utils";
import { ReactNode } from "react";
export function Card({
children,
className,
...props
}: React.HTMLAttributes<HTMLDivElement> & {
children: ReactNode;
className?: string;
}) {
return (
<div
className={cn(
"rounded-xl border border-border bg-card/80 backdrop-blur-xl text-card-foreground shadow-sm hover:shadow-primary/5 hover:border-primary/20 transition-all duration-300 ease-in-out hover:-translate-y-0.5",
className
)}
{...props}
>
{children}
</div>
);
}
export function CardHeader({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return (
<div className={cn("flex flex-col space-y-1.5 p-6", className)}>
{children}
</div>
);
}
export function CardTitle({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return (
<h3
className={cn(
"font-semibold leading-none tracking-tight",
className
)}
>
{children}
</h3>
);
}
export function CardContent({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return (
<div className={cn("p-6 pt-0", className)}>{children}</div>
);
}
+102
View File
@@ -0,0 +1,102 @@
"use client";
import { useState } from "react";
import { Search } from "lucide-react";
export interface Column<T> {
header: string;
accessor: (row: T, index: number) => React.ReactNode;
className?: string;
}
interface DataTableProps<T> {
data: T[];
columns: Column<T>[];
searchPlaceholder?: string;
searchFilter?: (row: T, query: string) => boolean;
isLoading?: boolean;
}
export function DataTable<T>({
data,
columns,
searchPlaceholder = "Search...",
searchFilter,
isLoading
}: DataTableProps<T>) {
const [query, setQuery] = useState("");
const filteredData = searchFilter
? data.filter(row => searchFilter(row, query))
: data;
return (
<div className="space-y-4 animate-fade-in">
{searchFilter && (
<div className="relative group">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground transition-colors group-focus-within:text-primary" />
<input
type="text"
placeholder={searchPlaceholder}
value={query}
onChange={(e) => 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"
/>
</div>
)}
<div className="rounded-xl border border-border overflow-hidden bg-card/50 backdrop-blur-xl shadow-sm">
<div className="overflow-x-auto">
<table className="w-full text-sm text-left">
<thead className="bg-secondary/30 text-muted-foreground border-b border-border/50">
<tr>
{columns.map((col, i) => (
<th key={i} className={`px-4 py-3.5 font-medium whitespace-nowrap tracking-wide ${col.className || ""}`}>
{col.header}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-border/50">
{isLoading ? (
<tr>
<td colSpan={columns.length} className="px-4 py-12 text-center text-muted-foreground">
<div className="flex flex-col items-center justify-center gap-3 animate-pulse">
<div className="relative">
<div className="absolute inset-0 bg-primary/20 rounded-full blur-xl"></div>
<svg className="animate-spin relative h-8 w-8 text-primary" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
<span className="text-sm font-medium tracking-wide">Loading data...</span>
</div>
</td>
</tr>
) : filteredData.length === 0 ? (
<tr>
<td colSpan={columns.length} className="px-4 py-12 text-center">
<div className="flex flex-col items-center justify-center text-muted-foreground">
<Search className="h-8 w-8 mb-3 opacity-20" />
<span className="text-sm font-medium">No data found</span>
{query && <span className="text-xs mt-1 opacity-70">Try adjusting your search query</span>}
</div>
</td>
</tr>
) : (
filteredData.map((row, i) => (
<tr key={i} className="hover:bg-primary/5 transition-colors duration-200 group">
{columns.map((col, j) => (
<td key={j} className={`px-4 py-3 whitespace-nowrap text-foreground group-hover:text-white transition-colors ${col.className || ""}`}>
{col.accessor(row, i)}
</td>
))}
</tr>
))
)}
</tbody>
</table>
</div>
</div>
</div>
);
}
+61
View File
@@ -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 (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-0">
{/* Backdrop */}
<div
className="fixed inset-0 bg-black/60 backdrop-blur-sm transition-opacity"
onClick={onClose}
/>
{/* Dialog */}
<div className={cn(
"relative z-50 w-full max-w-md transform overflow-hidden rounded-xl border border-border bg-card shadow-2xl transition-all sm:my-8",
className
)}>
<div className="flex items-center justify-between border-b border-border px-6 py-4">
<h3 className="text-lg font-semibold text-white tracking-tight">{title}</h3>
<button
onClick={onClose}
className="rounded-md p-1 text-muted-foreground hover:bg-secondary hover:text-white transition-colors focus:outline-none focus:ring-2 focus:ring-primary/50"
>
<span className="sr-only">Close</span>
<X className="h-5 w-5" />
</button>
</div>
<div className="px-6 py-4">
{children}
</div>
</div>
</div>
);
}
+49
View File
@@ -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<TabProps>[];
}
export function Tabs({ children }: TabsProps) {
const [activeTab, setActiveTab] = useState(0);
return (
<div className="flex flex-col space-y-4">
<div className="flex space-x-1 border-b border-white/10 overflow-x-auto pb-1">
{React.Children.map(children, (child, index) => {
if (!React.isValidElement(child)) return null;
const isActive = index === activeTab;
return (
<button
key={index}
onClick={() => setActiveTab(index)}
className={`
px-4 py-2 text-sm font-medium rounded-t-lg transition-colors whitespace-nowrap
${isActive
? 'bg-blue-600/20 text-blue-400 border-b-2 border-blue-500'
: 'text-gray-400 hover:text-gray-200 hover:bg-white/5'}
`}
>
{child.props.label}
</button>
);
})}
</div>
<div className="mt-4">
{React.Children.map(children, (child, index) => {
if (index !== activeTab) return null;
return child;
})}
</div>
</div>
);
}
+141
View File
@@ -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<string>()
.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 (
<div className="relative w-full aspect-[2/1] bg-secondary/20 rounded-xl overflow-hidden border border-border/50 backdrop-blur-sm">
<div className="absolute top-4 left-4 z-10 flex flex-col gap-1 pointer-events-none bg-card/80 p-3 rounded-lg border border-border/50 backdrop-blur-md">
<div className="text-sm font-medium text-white flex items-center gap-2">
Heatmap Activity
</div>
<div className="flex items-center gap-2 mt-2">
<div className="text-xs text-muted-foreground">Low</div>
<div className="w-24 h-2 rounded-full bg-gradient-to-r from-slate-800 to-sky-400 shadow-sm"></div>
<div className="text-xs text-muted-foreground">High</div>
</div>
</div>
{/* Tooltip Overlay */}
{tooltipContent && (
<div className="absolute bottom-4 left-4 z-10 bg-card/90 backdrop-blur-md border border-border/50 px-4 py-2.5 rounded-lg shadow-lg pointer-events-none animate-fade-in">
<div className="text-sm font-semibold text-white">{tooltipContent.split(':')[0]}</div>
<div className="text-xs text-sky-400 mt-1 font-medium">{tooltipContent.split(':')[1]}</div>
</div>
)}
<ComposableMap
projection="geoMercator"
projectionConfig={{
scale: 130,
center: [0, 30]
}}
width={800}
height={400}
className="w-full h-full"
>
<ZoomableGroup zoom={1} maxZoom={4} translateExtent={[[-100, -100], [900, 500]]}>
<Sphere stroke="rgba(255,255,255,0.05)" strokeWidth={0.5} id="sphere" fill="transparent" />
<Graticule stroke="rgba(255,255,255,0.05)" strokeWidth={0.5} />
<Geographies geography={geoUrl}>
{({ 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 (
<Geography
key={geo.rsmKey}
geography={geo}
fill={color}
stroke="rgba(255,255,255,0.1)"
strokeWidth={0.5}
style={{
default: { outline: "none" },
hover: {
fill: "#0ea5e9", // sky-500 on hover
outline: "none",
transition: "all 200ms ease"
},
pressed: { outline: "none" }
}}
onMouseEnter={() => {
if (d) {
setTooltipContent(`${d.label}: ${fmtBytes(d.value)}`);
} else {
setTooltipContent(`${geo.properties.name}: No data`);
}
}}
onMouseLeave={() => {
setTooltipContent("");
}}
/>
);
})
}
</Geographies>
</ZoomableGroup>
</ComposableMap>
</div>
);
};
export const WorldMap = memo(WorldMapComponent);
+152
View File
@@ -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<string, string> = {
"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<Agent[]> {
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<Agent | null> {
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 };
}
}
+85
View File
@@ -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<T>(endpoint: string, limit: number) {
const [data, setData] = useState<T[]>([]);
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetcher<T[]>(`${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<RegionStat>('/regions', limit);
export const useCities = (limit = 50) => useGenericFetch<CityStat>('/cities', limit);
export const useVlans = (limit = 50) => useGenericFetch<VlanStat>('/vlans', limit);
export const useInterfaces = (limit = 50) => useGenericFetch<InterfaceStat>('/interfaces', limit);
export const useIpVersions = (limit = 50) => useGenericFetch<IpVersionStat>('/ip-versions', limit);
export const useRemoteIps = (limit = 50) => useGenericFetch<RemoteIpStat>('/remote-ips', limit);
export const useMacBandwidth = (limit = 50) => useGenericFetch<MacBandwidthStat>('/mac-bandwidth', limit);
export const useFlowTypes = (limit = 50) => useGenericFetch<FlowTypeStat>('/flow-types', limit);
export const useFlowOrigins = (limit = 50) => useGenericFetch<FlowOriginStat>('/flow-origins', limit);
// DPI Analytics Hooks
export const useNetbios = (limit = 50) => useGenericFetch<NetbiosStat>('/netbios', limit);
export const useDiscoveryOs = (limit = 50) => useGenericFetch<DiscoveryOsStat>('/discovery-os', limit);
export const useDhcpFingerprints = (limit = 50) => useGenericFetch<DhcpFingerprintStat>('/dhcp-fingerprints', limit);
export const useHttpUserAgents = (limit = 50) => useGenericFetch<HttpUserAgentStat>('/http-user-agents', limit);
export const useSniHostnames = (limit = 50) => useGenericFetch<SniHostnameStat>('/sni-hostnames', limit);
export const useSslServerCn = (limit = 50) => useGenericFetch<SslServerCnStat>('/ssl-server-cn', limit);
export const useQuicHostnames = (limit = 50) => useGenericFetch<QuicHostnameStat>('/quic-hostnames', limit);
export const useBittorrentHashes = (limit = 50) => useGenericFetch<BittorrentHashStat>('/bittorrent-hashes', limit);
export const useSshVersions = (limit = 50) => useGenericFetch<SshVersionStat>('/ssh-versions', limit);
export const useMdnsHostnames = (limit = 50) => useGenericFetch<MdnsHostnameStat>('/mdns-hostnames', limit);
// Intelligence Detail Hooks
export const useIntelCryptoMining = (limit = 50) => useGenericFetch<IntelCryptoMining>('/intelligence/crypto-mining', limit);
export const useIntelDeviceDiscovery = (limit = 50) => useGenericFetch<IntelDeviceDiscovery>('/intelligence/device-discovery', limit);
export const useIntelEncryptionAudit = (limit = 50) => useGenericFetch<IntelEncryptionAudit>('/intelligence/encryption-audit', limit);
export const useIntelInsecureProtocols = (limit = 50) => useGenericFetch<IntelInsecureProtocol>('/intelligence/insecure-protocols', limit);
export const useIntelIpReputation = (limit = 50) => useGenericFetch<IntelIpReputation>('/intelligence/ip-reputation', limit);
export const useIntelServerDiscovery = (limit = 50) => useGenericFetch<IntelServerDiscovery>('/intelligence/server-discovery', limit);
export const useIntelTorDetection = (limit = 50) => useGenericFetch<IntelTorDetection>('/intelligence/tor', limit);
export const useIntelUnencryptedPasswords = (limit = 50) => useGenericFetch<IntelUnencryptedPassword>('/intelligence/unencrypted-passwords', limit);
export const useIntelVpnDetection = (limit = 50) => useGenericFetch<IntelVpnDetection>('/intelligence/vpn', limit);
+460
View File
@@ -0,0 +1,460 @@
import { useState, useEffect } from 'react';
// Common Response Wrapper
export interface ApiResponse<T> {
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<T>(endpoint: string): Promise<T> {
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<DashboardSummary | null>(null);
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetcher<DashboardSummary>('/summary')
.then(setData)
.catch(setError)
.finally(() => setIsLoading(false));
}, []);
return { data, error, isLoading };
}
export function useTimeline(points: number = 60) {
const [data, setData] = useState<TimelinePoint[]>([]);
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetcher<TimelinePoint[]>(`/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<AppStat[]>([]);
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetcher<AppStat[]>(`/apps?limit=${limit}`)
.then((res) => setData(res || []))
.catch(setError)
.finally(() => setIsLoading(false));
}, [limit]);
return { data, error, isLoading };
}
export function useTopProtocols() {
const [data, setData] = useState<ProtocolStat[]>([]);
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetcher<ProtocolStat[]>('/protocols')
.then((res) => setData(res || []))
.catch(setError)
.finally(() => setIsLoading(false));
}, []);
return { data, error, isLoading };
}
export function useDevices(limit: number = 50) {
const [data, setData] = useState<DeviceStat[]>([]);
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetcher<DeviceStat[]>(`/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<ThreatStat[]>([]);
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetcher<ThreatStat[]>(`/threats?limit=${limit}`)
.then((res) => setData(res || []))
.catch(setError)
.finally(() => setIsLoading(false));
}, [limit]);
return { data, error, isLoading };
}
export function useAppCategories() {
const [data, setData] = useState<AppCategoryStat[]>([]);
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetcher<AppCategoryStat[]>('/app-categories')
.then((res) => setData(res || []))
.catch(setError)
.finally(() => setIsLoading(false));
}, []);
return { data, error, isLoading };
}
export function useContinents() {
const [data, setData] = useState<ContinentStat[]>([]);
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetcher<ContinentStat[]>('/continents')
.then((res) => setData(res || []))
.catch(setError)
.finally(() => setIsLoading(false));
}, []);
return { data, error, isLoading };
}
export function useFlows(limit: number = 50) {
const [data, setData] = useState<FlowStat[]>([]);
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetcher<FlowStat[]>(`/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<CountryStat[]>([]);
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetcher<CountryStat[]>(`/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<DnsStat[]>([]);
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetcher<DnsStat[]>(`/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<EventStat[]>([]);
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetcher<EventStat[]>(`/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<TlsVersionStat[]>([]);
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetcher<TlsVersionStat[]>(`/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<TlsCipherStat[]>([]);
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetcher<TlsCipherStat[]>(`/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<TlsSecurityStat[]>([]);
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetcher<TlsSecurityStat[]>(`/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<IntelligenceStats | null>(null);
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetcher<IntelligenceStats>('/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<LookupApplicationsResponse | null>(null);
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
setIsLoading(true);
fetcher<LookupApplicationsResponse>(`/lookup/applications?page=${page}&limit=${limit}&search=${encodeURIComponent(search)}`)
.then(setData)
.catch(setError)
.finally(() => setIsLoading(false));
}, [page, limit, search]);
return { data, error, isLoading };
}
+13
View File
@@ -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];
}