diff --git a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/monitoring.js b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/monitoring.js
index a0e1d1ab..b8ba48d7 100644
--- a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/monitoring.js
+++ b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/monitoring.js
@@ -29,6 +29,7 @@ return view.extend({
cpuHistory: [],
memoryHistory: [],
diskHistory: [],
+ loadHistory: [],
networkHistory: [],
maxDataPoints: 60,
latestHealth: {},
@@ -50,15 +51,23 @@ return view.extend({
addDataPoint: function(health) {
var timestamp = Date.now();
- this.cpuHistory.push({ time: timestamp, value: (health.cpu && health.cpu.percent) || 0 });
- this.memoryHistory.push({ time: timestamp, value: (health.memory && health.memory.percent) || 0 });
- this.diskHistory.push({ time: timestamp, value: (health.disk && health.disk.percent) || 0 });
+ // API returns usage_percent, not percent
+ this.cpuHistory.push({ time: timestamp, value: (health.cpu && (health.cpu.usage_percent || health.cpu.percent)) || 0 });
+ this.memoryHistory.push({ time: timestamp, value: (health.memory && (health.memory.usage_percent || health.memory.percent)) || 0 });
+ this.diskHistory.push({ time: timestamp, value: (health.disk && (health.disk.usage_percent || health.disk.percent)) || 0 });
+
+ // System load - parse from string "2.14 1.86 1.70" or array, scale to percentage (assume 4 cores = 400% max)
+ var loadStr = (health.cpu && health.cpu.load) || (health.load && (Array.isArray(health.load) ? health.load[0] : health.load)) || '0';
+ var loadAvg = Array.isArray(loadStr) ? loadStr[0] : (typeof loadStr === 'string' ? parseFloat(loadStr.split(' ')[0]) : loadStr);
+ var numCores = (health.cpu && health.cpu.count) || 4;
+ var loadPercent = Math.min(100, (parseFloat(loadAvg) / numCores) * 100);
+ this.loadHistory.push({ time: timestamp, value: loadPercent, raw: loadAvg });
var netRx = (health.network && health.network.rx_bytes) || 0;
var netTx = (health.network && health.network.tx_bytes) || 0;
this.networkHistory.push({ time: timestamp, rx: netRx, tx: netTx });
- ['cpuHistory', 'memoryHistory', 'diskHistory', 'networkHistory'].forEach(function(key) {
+ ['cpuHistory', 'memoryHistory', 'diskHistory', 'loadHistory', 'networkHistory'].forEach(function(key) {
if (this[key].length > this.maxDataPoints)
this[key].shift();
}, this);
@@ -73,8 +82,8 @@ return view.extend({
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox/monitoring.css') }),
SecuNav.renderTabs('monitoring'),
this.renderHeader(),
- this.renderChartsGrid(),
- this.renderCurrentStatsCard()
+ this.renderCurrentStatsCard(),
+ this.renderChartsGrid()
]);
this.updateCharts();
@@ -131,7 +140,7 @@ return view.extend({
this.renderChartCard('cpu', _('CPU Usage'), '%', '#6366f1'),
this.renderChartCard('memory', _('Memory Usage'), '%', '#22c55e'),
this.renderChartCard('disk', _('Disk Usage'), '%', '#f59e0b'),
- this.renderChartCard('network', _('Network Throughput'), 'B/s', '#3b82f6')
+ this.renderChartCard('load', _('System Load'), '', '#ec4899')
]);
},
@@ -167,7 +176,10 @@ return view.extend({
renderStatsTable: function() {
var snapshot = this.getLatestSnapshot();
var rates = this.getNetworkRateSummary();
- var load = (this.latestHealth.load && this.latestHealth.load[0]) || '0.00';
+ // Load from cpu.load string "2.14 1.86 1.70" or load array
+ var loadStr = (this.latestHealth.cpu && this.latestHealth.cpu.load) ||
+ (this.latestHealth.load && (Array.isArray(this.latestHealth.load) ? this.latestHealth.load[0] : this.latestHealth.load)) || '0.00';
+ var load = typeof loadStr === 'string' ? loadStr.split(' ')[0] : loadStr;
var stats = [
{ label: _('CPU Usage'), value: snapshot.cpu.value.toFixed(1) + '%', icon: 'โก' },
@@ -193,7 +205,7 @@ return view.extend({
this.drawChart('cpu', this.cpuHistory, '#6366f1');
this.drawChart('memory', this.memoryHistory, '#22c55e');
this.drawChart('disk', this.diskHistory, '#f59e0b');
- this.drawNetworkChart();
+ this.drawLoadChart();
},
drawChart: function(type, data, color) {
@@ -282,6 +294,42 @@ return view.extend({
}
},
+ drawLoadChart: function() {
+ var svg = document.getElementById('chart-load');
+ var currentEl = document.getElementById('current-load');
+ if (!svg || this.loadHistory.length === 0)
+ return;
+
+ var width = 600;
+ var height = 200;
+ var padding = 12;
+
+ var values = this.loadHistory.map(function(d) { return d.value; });
+ var maxValue = Math.max(100, Math.max.apply(Math, values));
+ var minValue = 0;
+ var pathPoints = this.loadHistory.map(function(point, idx) {
+ var x = padding + (width - 2 * padding) * (idx / Math.max(1, this.maxDataPoints - 1));
+ var y = height - padding - ((point.value - minValue) / (maxValue - minValue)) * (height - 2 * padding);
+ return x + ',' + y;
+ }, this).join(' ');
+
+ svg.innerHTML = '';
+
+ svg.appendChild(E('polyline', {
+ 'points': pathPoints,
+ 'fill': 'none',
+ 'stroke': '#ec4899',
+ 'stroke-width': '2',
+ 'stroke-linejoin': 'round',
+ 'stroke-linecap': 'round'
+ }));
+
+ if (currentEl) {
+ var lastPoint = this.loadHistory[this.loadHistory.length - 1];
+ currentEl.textContent = (lastPoint.raw || '0.00');
+ }
+ },
+
updateCurrentStats: function() {
var statsContainer = document.getElementById('current-stats');
if (statsContainer)
diff --git a/package/secubox/luci-app-ndpid/Makefile b/package/secubox/luci-app-ndpid/Makefile
index 821a5a15..2d0e2650 100644
--- a/package/secubox/luci-app-ndpid/Makefile
+++ b/package/secubox/luci-app-ndpid/Makefile
@@ -7,8 +7,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-ndpid
-PKG_VERSION:=0.9.1
-PKG_RELEASE:=2
+PKG_VERSION:=1.0.0
+PKG_RELEASE:=1
PKG_ARCH:=all
PKG_LICENSE:=Apache-2.0
diff --git a/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/ndpid/api.js b/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/ndpid/api.js
index b7dd5497..c4333844 100644
--- a/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/ndpid/api.js
+++ b/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/ndpid/api.js
@@ -20,6 +20,18 @@ var callRealtimeFlows = rpc.declare({
expect: { }
});
+var callDetailedFlows = rpc.declare({
+ object: 'luci.ndpid',
+ method: 'get_detailed_flows',
+ expect: { flows: [] }
+});
+
+var callCategories = rpc.declare({
+ object: 'luci.ndpid',
+ method: 'get_categories',
+ expect: { categories: [] }
+});
+
var callInterfaceStats = rpc.declare({
object: 'luci.ndpid',
method: 'get_interface_stats',
@@ -136,9 +148,11 @@ return baseclass.extend({
// Read methods
getServiceStatus: callServiceStatus,
getRealtimeFlows: callRealtimeFlows,
+ getDetailedFlows: callDetailedFlows,
getInterfaceStats: callInterfaceStats,
getTopApplications: callTopApplications,
getTopProtocols: callTopProtocols,
+ getCategories: callCategories,
getConfig: callConfig,
getDashboard: callDashboard,
getInterfaces: callInterfaces,
diff --git a/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/ndpid/dashboard.css b/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/ndpid/dashboard.css
index 607e0cda..82ed7b7f 100644
--- a/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/ndpid/dashboard.css
+++ b/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/ndpid/dashboard.css
@@ -527,3 +527,276 @@
background: var(--ndpi-border);
border-radius: 4px;
}
+
+/* Grid Layouts */
+.ndpi-grid-2 {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
+ gap: 20px;
+ margin-bottom: 20px;
+}
+
+.ndpi-card-wide {
+ grid-column: 1 / -1;
+}
+
+/* Applications List */
+.ndpi-apps-list {
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+}
+
+.ndpi-app-item {
+ padding: 12px 0;
+ border-bottom: 1px solid var(--ndpi-border);
+}
+
+.ndpi-app-item:last-child {
+ border-bottom: none;
+}
+
+.ndpi-app-header {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 8px;
+}
+
+.ndpi-app-icon {
+ font-size: 20px;
+ width: 28px;
+ text-align: center;
+}
+
+.ndpi-app-info {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ min-width: 0;
+}
+
+.ndpi-app-name {
+ font-weight: 600;
+ color: var(--ndpi-text-primary);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.ndpi-app-host {
+ font-size: 11px;
+ color: var(--ndpi-text-muted);
+ font-family: var(--ndpi-font-mono);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.ndpi-app-bytes {
+ font-family: var(--ndpi-font-mono);
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--ndpi-accent-cyan);
+ margin-left: auto;
+}
+
+.ndpi-app-bar {
+ height: 6px;
+ background: var(--ndpi-bg-primary);
+ border-radius: 3px;
+ overflow: hidden;
+}
+
+.ndpi-app-bar-fill {
+ height: 100%;
+ border-radius: 3px;
+ transition: width 0.5s ease;
+}
+
+.ndpi-app-meta {
+ font-size: 10px;
+ color: var(--ndpi-text-muted);
+ margin-top: 6px;
+}
+
+/* Categories List */
+.ndpi-categories-list {
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+}
+
+.ndpi-category-item {
+ padding: 10px 0;
+ border-bottom: 1px solid var(--ndpi-border);
+}
+
+.ndpi-category-item:last-child {
+ border-bottom: none;
+}
+
+.ndpi-category-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 8px;
+}
+
+.ndpi-category-name {
+ font-weight: 600;
+ font-size: 14px;
+}
+
+.ndpi-category-bytes {
+ font-family: var(--ndpi-font-mono);
+ font-size: 12px;
+ color: var(--ndpi-text-secondary);
+}
+
+.ndpi-category-bar {
+ height: 6px;
+ background: var(--ndpi-bg-primary);
+ border-radius: 3px;
+ overflow: hidden;
+}
+
+.ndpi-category-bar-fill {
+ height: 100%;
+ border-radius: 3px;
+ transition: width 0.5s ease;
+}
+
+.ndpi-category-meta {
+ font-size: 10px;
+ color: var(--ndpi-text-muted);
+ margin-top: 6px;
+}
+
+/* Flows Table */
+.ndpi-flows-table-container {
+ max-height: 500px;
+ overflow-y: auto;
+}
+
+.ndpi-flows-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 12px;
+}
+
+.ndpi-flows-table th {
+ text-align: left;
+ padding: 10px 12px;
+ background: var(--ndpi-bg-tertiary);
+ font-size: 10px;
+ font-weight: 600;
+ text-transform: uppercase;
+ color: var(--ndpi-text-muted);
+ border-bottom: 1px solid var(--ndpi-border);
+ position: sticky;
+ top: 0;
+ z-index: 1;
+}
+
+.ndpi-flow-row {
+ transition: background 0.2s;
+}
+
+.ndpi-flow-row:hover {
+ background: rgba(6, 182, 212, 0.05);
+}
+
+.ndpi-flow-row td {
+ padding: 10px 12px;
+ border-bottom: 1px solid var(--ndpi-border);
+ vertical-align: middle;
+}
+
+.ndpi-flow-ended {
+ opacity: 0.5;
+}
+
+.ndpi-flow-app {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ min-width: 150px;
+}
+
+.ndpi-flow-src, .ndpi-flow-dst {
+ font-family: var(--ndpi-font-mono);
+ font-size: 11px;
+ white-space: nowrap;
+}
+
+.ndpi-flow-arrow {
+ color: var(--ndpi-text-muted);
+ font-size: 14px;
+}
+
+/* Protocol Badges */
+.ndpi-proto-badge {
+ display: inline-block;
+ padding: 2px 8px;
+ border-radius: 4px;
+ font-size: 10px;
+ font-weight: 600;
+ text-transform: uppercase;
+}
+
+.ndpi-proto-tcp {
+ background: rgba(14, 165, 233, 0.2);
+ color: #0ea5e9;
+}
+
+.ndpi-proto-udp {
+ background: rgba(16, 185, 129, 0.2);
+ color: #10b981;
+}
+
+.ndpi-proto-icmp {
+ background: rgba(245, 158, 11, 0.2);
+ color: #f59e0b;
+}
+
+/* Category Badge */
+.ndpi-category-badge {
+ display: inline-block;
+ padding: 2px 8px;
+ border-radius: 4px;
+ font-size: 10px;
+ font-weight: 500;
+ color: white;
+ white-space: nowrap;
+}
+
+/* Flow State */
+.ndpi-state-active {
+ color: var(--ndpi-accent-green);
+ font-size: 14px;
+}
+
+.ndpi-state-ended {
+ color: var(--ndpi-text-muted);
+ font-size: 14px;
+}
+
+.ndpi-empty-row {
+ text-align: center;
+ padding: 40px 20px;
+ color: var(--ndpi-text-muted);
+}
+
+.ndpi-empty-hint {
+ font-size: 12px;
+ color: var(--ndpi-text-muted);
+ margin-top: 8px;
+}
+
+/* Responsive for grids */
+@media (max-width: 900px) {
+ .ndpi-grid-2 {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/view/ndpid/dashboard.js b/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/view/ndpid/dashboard.js
index e35783ba..112db59e 100644
--- a/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/view/ndpid/dashboard.js
+++ b/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/view/ndpid/dashboard.js
@@ -11,7 +11,56 @@ return view.extend({
pollActive: true,
load: function() {
- return api.getAllData();
+ return Promise.all([
+ api.getAllData(),
+ api.getCategories().catch(function() { return { categories: [] }; })
+ ]).then(function(results) {
+ var data = results[0];
+ data.categories = results[1];
+ return data;
+ });
+ },
+
+ getAppIcon: function(app, category) {
+ var icons = {
+ 'HTTP': '๐', 'HTTPS': '๐', 'TLS': '๐', 'SSL': '๐',
+ 'DNS': '๐ก', 'NTP': '๐', 'DHCP': '๐',
+ 'SSH': '๐ฅ๏ธ', 'Telnet': '๐ป',
+ 'YouTube': 'โถ๏ธ', 'Netflix': '๐ฌ', 'Twitch': '๐ฎ',
+ 'Facebook': '๐ค', 'Twitter': '๐ฆ', 'Instagram': '๐ท', 'TikTok': '๐ต',
+ 'WhatsApp': '๐ฌ', 'Telegram': 'โ๏ธ', 'Discord': '๐ง',
+ 'BitTorrent': '๐ฅ', 'eDonkey': '๐ฅ',
+ 'Spotify': '๐ต', 'AppleMusic': '๐ต',
+ 'Dropbox': '๐ฆ', 'GoogleDrive': '๐ฆ', 'OneDrive': '๐ฆ',
+ 'Zoom': '๐น', 'Teams': '๐ฅ', 'Skype': '๐',
+ 'VPN': '๐ก๏ธ', 'OpenVPN': '๐ก๏ธ', 'WireGuard': '๐ก๏ธ',
+ 'QUIC': 'โก', 'HTTP2': 'โก',
+ 'SMTP': '๐ง', 'IMAP': '๐ง', 'POP3': '๐ง',
+ 'FTP': '๐', 'SFTP': '๐', 'SMB': '๐',
+ 'ICMP': '๐ถ', 'IGMP': '๐ก',
+ 'Unknown': 'โ'
+ };
+ return icons[app] || icons[category] || '๐ฆ';
+ },
+
+ getCategoryColor: function(category) {
+ var colors = {
+ 'Web': '#3b82f6',
+ 'Video': '#ef4444',
+ 'Streaming': '#f59e0b',
+ 'SocialNetwork': '#ec4899',
+ 'Chat': '#8b5cf6',
+ 'VoIP': '#10b981',
+ 'Game': '#06b6d4',
+ 'Download': '#f97316',
+ 'Cloud': '#6366f1',
+ 'VPN': '#14b8a6',
+ 'Mail': '#84cc16',
+ 'FileTransfer': '#a855f7',
+ 'Network': '#64748b',
+ 'Unknown': '#94a3b8'
+ };
+ return colors[category] || '#64748b';
},
updateDashboard: function(data) {
@@ -46,7 +95,7 @@ return view.extend({
});
// Update interface stats
- var interfaces = (data.interfaces || {}).interfaces || [];
+ var interfaces = Array.isArray(data.interfaces) ? data.interfaces : (data.interfaces || {}).interfaces || [];
interfaces.forEach(function(iface) {
var card = document.querySelector('.ndpi-iface-card[data-iface="' + iface.name + '"]');
if (!card) return;
@@ -121,9 +170,11 @@ return view.extend({
var service = dashboard.service || {};
var flows = dashboard.flows || {};
var system = dashboard.system || {};
- var interfaces = (data.interfaces || {}).interfaces || [];
- var applications = (data.applications || {}).applications || [];
- var protocols = (data.protocols || {}).protocols || [];
+ // Handle both array and object formats from API
+ var interfaces = Array.isArray(data.interfaces) ? data.interfaces : (data.interfaces || {}).interfaces || [];
+ var applications = Array.isArray(data.applications) ? data.applications : (data.applications || {}).applications || [];
+ var protocols = Array.isArray(data.protocols) ? data.protocols : (data.protocols || {}).protocols || [];
+ var categories = Array.isArray(data.categories) ? data.categories : (data.categories || {}).categories || [];
var view = E('div', { 'class': 'ndpid-dashboard' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('ndpid/dashboard.css') }),
@@ -276,43 +327,81 @@ return view.extend({
)
]),
- // Top Applications
- E('div', { 'class': 'ndpi-card' }, [
- E('div', { 'class': 'ndpi-card-header' }, [
- E('div', { 'class': 'ndpi-card-title' }, [
- E('span', { 'class': 'ndpi-card-title-icon' }, '๐ฑ'),
- 'Top Applications'
- ])
- ]),
- E('div', { 'class': 'ndpi-card-body' },
- applications.length > 0 ?
- E('div', { 'class': 'ndpi-table-container' }, [
- E('table', { 'class': 'ndpi-table' }, [
- E('thead', {}, [
- E('tr', {}, [
- E('th', {}, 'Application'),
- E('th', {}, 'Flows'),
- E('th', {}, 'Traffic')
- ])
- ]),
- E('tbody', {},
- applications.map(function(app) {
- return E('tr', {}, [
- E('td', {}, [
- E('span', { 'class': 'ndpi-app-name' }, app.name || 'unknown')
+ // Grid layout for Applications and Categories
+ E('div', { 'class': 'ndpi-grid-2' }, [
+ // Top Applications
+ E('div', { 'class': 'ndpi-card' }, [
+ E('div', { 'class': 'ndpi-card-header' }, [
+ E('div', { 'class': 'ndpi-card-title' }, [
+ E('span', { 'class': 'ndpi-card-title-icon' }, '๐ฑ'),
+ 'Top Applications'
+ ]),
+ E('div', { 'class': 'ndpi-card-badge' }, applications.length + ' detected')
+ ]),
+ E('div', { 'class': 'ndpi-card-body' },
+ applications.length > 0 ?
+ E('div', { 'class': 'ndpi-apps-list' },
+ (function() {
+ var maxBytes = Math.max.apply(null, applications.map(function(a) { return a.bytes || 0; })) || 1;
+ return applications.slice(0, 8).map(function(app) {
+ var pct = Math.round(((app.bytes || 0) / maxBytes) * 100);
+ return E('div', { 'class': 'ndpi-app-item' }, [
+ E('div', { 'class': 'ndpi-app-header' }, [
+ E('span', { 'class': 'ndpi-app-icon' }, self.getAppIcon(app.name, app.category)),
+ E('span', { 'class': 'ndpi-app-name' }, app.name || 'Unknown'),
+ E('span', { 'class': 'ndpi-app-bytes' }, api.formatBytes(app.bytes || 0))
]),
- E('td', { 'class': 'mono' }, api.formatNumber(app.flows)),
- E('td', { 'class': 'mono' }, api.formatBytes(app.bytes))
+ E('div', { 'class': 'ndpi-app-bar' }, [
+ E('div', { 'class': 'ndpi-app-bar-fill', 'style': 'width:' + pct + '%;background:' + self.getCategoryColor(app.category) })
+ ]),
+ E('div', { 'class': 'ndpi-app-meta' }, (app.flows || 0) + ' flows ยท ' + (app.category || 'Unknown'))
]);
- })
- )
+ });
+ })()
+ ) :
+ E('div', { 'class': 'ndpi-empty' }, [
+ E('div', { 'class': 'ndpi-empty-icon' }, '๐ฑ'),
+ E('div', { 'class': 'ndpi-empty-text' }, 'No applications detected yet'),
+ E('p', {}, 'Generate network traffic to see app detection')
])
- ]) :
- E('div', { 'class': 'ndpi-empty' }, [
- E('div', { 'class': 'ndpi-empty-icon' }, '๐ฑ'),
- E('div', { 'class': 'ndpi-empty-text' }, 'No applications detected yet')
- ])
- )
+ )
+ ]),
+
+ // Traffic Categories
+ E('div', { 'class': 'ndpi-card' }, [
+ E('div', { 'class': 'ndpi-card-header' }, [
+ E('div', { 'class': 'ndpi-card-title' }, [
+ E('span', { 'class': 'ndpi-card-title-icon' }, '๐ท๏ธ'),
+ 'Traffic Categories'
+ ]),
+ E('div', { 'class': 'ndpi-card-badge' }, categories.length + ' types')
+ ]),
+ E('div', { 'class': 'ndpi-card-body' },
+ categories.length > 0 ?
+ E('div', { 'class': 'ndpi-categories-list' },
+ (function() {
+ var maxBytes = Math.max.apply(null, categories.map(function(c) { return c.bytes || 0; })) || 1;
+ return categories.slice(0, 8).map(function(cat) {
+ var pct = Math.round(((cat.bytes || 0) / maxBytes) * 100);
+ return E('div', { 'class': 'ndpi-category-item' }, [
+ E('div', { 'class': 'ndpi-category-header' }, [
+ E('span', { 'class': 'ndpi-category-name', 'style': 'color:' + self.getCategoryColor(cat.name) }, cat.name),
+ E('span', { 'class': 'ndpi-category-bytes' }, api.formatBytes(cat.bytes || 0))
+ ]),
+ E('div', { 'class': 'ndpi-category-bar' }, [
+ E('div', { 'class': 'ndpi-category-bar-fill', 'style': 'width:' + pct + '%;background:' + self.getCategoryColor(cat.name) })
+ ]),
+ E('div', { 'class': 'ndpi-category-meta' }, (cat.apps || 0) + ' apps ยท ' + (cat.flows || 0) + ' flows')
+ ]);
+ });
+ })()
+ ) :
+ E('div', { 'class': 'ndpi-empty' }, [
+ E('div', { 'class': 'ndpi-empty-icon' }, '๐ท๏ธ'),
+ E('div', { 'class': 'ndpi-empty-text' }, 'No categories detected yet')
+ ])
+ )
+ ])
]),
// Top Protocols
diff --git a/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/view/ndpid/flows.js b/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/view/ndpid/flows.js
index 2779986e..5c33a52d 100644
--- a/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/view/ndpid/flows.js
+++ b/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/view/ndpid/flows.js
@@ -12,27 +12,85 @@ return view.extend({
load: function() {
return Promise.all([
- api.getRealtimeFlows(),
- api.getInterfaceStats(),
- api.getTopProtocols()
+ api.getRealtimeFlows().catch(function(e) { console.log('getRealtimeFlows error:', e); return {}; }),
+ api.getDetailedFlows().catch(function(e) { console.log('getDetailedFlows error:', e); return { flows: [] }; }),
+ api.getTopApplications().catch(function(e) { console.log('getTopApplications error:', e); return { applications: [] }; }),
+ api.getCategories().catch(function(e) { console.log('getCategories error:', e); return { categories: [] }; }),
+ api.getInterfaceStats().catch(function(e) { console.log('getInterfaceStats error:', e); return { interfaces: [] }; }),
+ api.getTopProtocols().catch(function(e) { console.log('getTopProtocols error:', e); return { protocols: [] }; })
]).then(function(results) {
+ console.log('nDPId flows.js load results:', results);
+ console.log('Detailed flows:', results[1]);
+ console.log('Applications:', results[2]);
+ console.log('Categories:', results[3]);
return {
- flows: results[0],
- interfaces: results[1],
- protocols: results[2]
+ status: results[0],
+ flows: results[1],
+ applications: results[2],
+ categories: results[3],
+ interfaces: results[4],
+ protocols: results[5]
};
});
},
- updateFlows: function(data) {
- var flows = data.flows || {};
+ getProtoName: function(proto) {
+ var protos = { '6': 'TCP', '17': 'UDP', '1': 'ICMP', 'tcp': 'TCP', 'udp': 'UDP', 'icmp': 'ICMP' };
+ return protos[proto] || proto || '?';
+ },
+
+ getAppIcon: function(app, category) {
+ var icons = {
+ 'HTTP': '๐', 'HTTPS': '๐', 'TLS': '๐', 'SSL': '๐',
+ 'DNS': '๐ก', 'NTP': '๐', 'DHCP': '๐',
+ 'SSH': '๐ฅ๏ธ', 'Telnet': '๐ป',
+ 'YouTube': 'โถ๏ธ', 'Netflix': '๐ฌ', 'Twitch': '๐ฎ',
+ 'Facebook': '๐ค', 'Twitter': '๐ฆ', 'Instagram': '๐ท', 'TikTok': '๐ต',
+ 'WhatsApp': '๐ฌ', 'Telegram': 'โ๏ธ', 'Discord': '๐ง',
+ 'BitTorrent': '๐ฅ', 'eDonkey': '๐ฅ',
+ 'Spotify': '๐ต', 'AppleMusic': '๐ต',
+ 'Dropbox': '๐ฆ', 'GoogleDrive': '๐ฆ', 'OneDrive': '๐ฆ',
+ 'Zoom': '๐น', 'Teams': '๐ฅ', 'Skype': '๐',
+ 'VPN': '๐ก๏ธ', 'OpenVPN': '๐ก๏ธ', 'WireGuard': '๐ก๏ธ',
+ 'QUIC': 'โก', 'HTTP2': 'โก',
+ 'SMTP': '๐ง', 'IMAP': '๐ง', 'POP3': '๐ง',
+ 'FTP': '๐', 'SFTP': '๐', 'SMB': '๐',
+ 'ICMP': '๐ถ', 'IGMP': '๐ก',
+ 'Unknown': 'โ'
+ };
+ return icons[app] || icons[category] || '๐ฆ';
+ },
+
+ getCategoryColor: function(category) {
+ var colors = {
+ 'Web': '#3b82f6',
+ 'Video': '#ef4444',
+ 'Streaming': '#f59e0b',
+ 'SocialNetwork': '#ec4899',
+ 'Chat': '#8b5cf6',
+ 'VoIP': '#10b981',
+ 'Game': '#06b6d4',
+ 'Download': '#f97316',
+ 'Cloud': '#6366f1',
+ 'VPN': '#14b8a6',
+ 'Mail': '#84cc16',
+ 'FileTransfer': '#a855f7',
+ 'Network': '#64748b',
+ 'Unknown': '#94a3b8'
+ };
+ return colors[category] || '#64748b';
+ },
+
+ updateData: function(data) {
+ var self = this;
// Update flow counts
+ var status = data.status || {};
var activeEl = document.querySelector('.ndpi-flows-active');
var totalEl = document.querySelector('.ndpi-flows-total');
if (activeEl) {
- var newActive = api.formatNumber(flows.flows_active || 0);
+ var newActive = api.formatNumber(status.flows_active || 0);
if (activeEl.textContent !== newActive) {
activeEl.textContent = newActive;
activeEl.classList.add('ndpi-value-updated');
@@ -41,23 +99,61 @@ return view.extend({
}
if (totalEl) {
- var newTotal = api.formatNumber(flows.flow_count || 0);
- if (totalEl.textContent !== newTotal) {
- totalEl.textContent = newTotal;
- }
+ totalEl.textContent = api.formatNumber(status.flow_count || 0);
}
- // Update interface stats
- var interfaces = (data.interfaces || {}).interfaces || [];
- interfaces.forEach(function(iface) {
- var row = document.querySelector('.ndpi-iface-row[data-iface="' + iface.name + '"]');
- if (!row) return;
+ // Update flows table
+ var flows = Array.isArray(data.flows) ? data.flows : (data.flows || {}).flows || [];
+ var tbody = document.querySelector('.ndpi-flows-tbody');
+ if (tbody && flows.length > 0) {
+ tbody.innerHTML = '';
+ flows.slice(0, 50).forEach(function(flow) {
+ var tr = document.createElement('tr');
+ tr.className = 'ndpi-flow-row ' + (flow.state === 'ended' ? 'ndpi-flow-ended' : 'ndpi-flow-active');
+ tr.innerHTML = [
+ '
',
+ '' + self.getAppIcon(flow.app, flow.category) + '',
+ ' ',
+ '' + (flow.app || 'Unknown') + '',
+ '' + (flow.hostname || '') + '',
+ ' ',
+ ' | ',
+ '' + flow.src_ip + ':' + flow.src_port + ' | ',
+ 'โ | ',
+ '' + flow.dst_ip + ':' + flow.dst_port + ' | ',
+ '' + self.getProtoName(flow.proto) + ' | ',
+ '' + (flow.category || 'Unknown') + ' | ',
+ '' + api.formatBytes((flow.bytes_rx || 0) + (flow.bytes_tx || 0)) + ' | ',
+ '' + (flow.state === 'active' ? 'โ' : 'โ') + ' | '
+ ].join('');
+ tbody.appendChild(tr);
+ });
+ } else if (tbody) {
+ tbody.innerHTML = '| No flows detected yet |
';
+ }
- row.querySelector('.ndpi-iface-tcp').textContent = api.formatNumber(iface.tcp);
- row.querySelector('.ndpi-iface-udp').textContent = api.formatNumber(iface.udp);
- row.querySelector('.ndpi-iface-icmp').textContent = api.formatNumber(iface.icmp);
- row.querySelector('.ndpi-iface-bytes').textContent = api.formatBytes(iface.ip_bytes);
- });
+ // Update top applications
+ var apps = Array.isArray(data.applications) ? data.applications : (data.applications || {}).applications || [];
+ var appsContainer = document.querySelector('.ndpi-apps-list');
+ if (appsContainer && apps.length > 0) {
+ var maxBytes = Math.max.apply(null, apps.map(function(a) { return a.bytes || 0; })) || 1;
+ appsContainer.innerHTML = '';
+ apps.slice(0, 10).forEach(function(app) {
+ var pct = Math.round(((app.bytes || 0) / maxBytes) * 100);
+ var div = document.createElement('div');
+ div.className = 'ndpi-app-item';
+ div.innerHTML = [
+ '',
+ '',
+ '' + (app.flows || 0) + ' flows ยท ' + (app.category || 'Unknown') + '
'
+ ].join('');
+ appsContainer.appendChild(div);
+ });
+ }
},
startPolling: function() {
@@ -68,12 +164,14 @@ return view.extend({
if (!this.pollActive) return Promise.resolve();
return Promise.all([
- api.getRealtimeFlows(),
- api.getInterfaceStats()
+ api.getRealtimeFlows().catch(function() { return {}; }),
+ api.getDetailedFlows().catch(function() { return { flows: [] }; }),
+ api.getTopApplications().catch(function() { return { applications: [] }; })
]).then(L.bind(function(results) {
- this.updateFlows({
- flows: results[0],
- interfaces: results[1]
+ this.updateData({
+ status: results[0],
+ flows: results[1],
+ applications: results[2]
});
}, this));
}, this), this.pollInterval);
@@ -86,25 +184,37 @@ return view.extend({
render: function(data) {
var self = this;
- var flows = data.flows || {};
- var interfaces = (data.interfaces || {}).interfaces || [];
- var protocols = (data.protocols || {}).protocols || [];
+ var status = data.status || {};
+
+ // Debug: log raw data
+ console.log('RENDER - raw data.flows:', data.flows);
+ console.log('RENDER - Array.isArray(data.flows):', Array.isArray(data.flows));
+
+ // Handle both array and object formats from API
+ var flows = Array.isArray(data.flows) ? data.flows : (data.flows || {}).flows || [];
+ var applications = Array.isArray(data.applications) ? data.applications : (data.applications || {}).applications || [];
+ var categories = Array.isArray(data.categories) ? data.categories : (data.categories || {}).categories || [];
+ var interfaces = Array.isArray(data.interfaces) ? data.interfaces : (data.interfaces || {}).interfaces || [];
+ var protocols = Array.isArray(data.protocols) ? data.protocols : (data.protocols || {}).protocols || [];
+
+ // Debug: log processed data
+ console.log('RENDER - processed flows.length:', flows.length);
+ console.log('RENDER - processed apps.length:', applications.length);
- // Calculate protocol totals
var totalPackets = protocols.reduce(function(sum, p) { return sum + (p.count || 0); }, 0);
- var view = E('div', { 'class': 'ndpid-dashboard' }, [
+ var view = E('div', { 'class': 'ndpid-dashboard ndpid-flows-page' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('ndpid/dashboard.css') }),
// Header
E('div', { 'class': 'ndpi-header' }, [
E('div', { 'class': 'ndpi-logo' }, [
- E('div', { 'class': 'ndpi-logo-icon' }, '๐'),
- E('div', { 'class': 'ndpi-logo-text' }, ['Flow ', E('span', {}, 'Statistics')])
+ E('div', { 'class': 'ndpi-logo-icon' }, '๐'),
+ E('div', { 'class': 'ndpi-logo-text' }, ['Deep Packet ', E('span', {}, 'Inspection')])
])
]),
- // Flow Summary
+ // Quick Stats
E('div', { 'class': 'ndpi-quick-stats' }, [
E('div', { 'class': 'ndpi-quick-stat', 'style': '--stat-gradient: linear-gradient(135deg, #10b981, #34d399)' }, [
E('div', { 'class': 'ndpi-quick-stat-header' }, [
@@ -112,8 +222,8 @@ return view.extend({
E('span', { 'class': 'ndpi-quick-stat-label' }, 'Active Flows')
]),
E('div', { 'class': 'ndpi-quick-stat-value ndpi-flows-active' },
- api.formatNumber(flows.flows_active || 0)),
- E('div', { 'class': 'ndpi-quick-stat-sub' }, 'Currently tracked')
+ api.formatNumber(status.flows_active || 0)),
+ E('div', { 'class': 'ndpi-quick-stat-sub' }, 'Real-time tracking')
]),
E('div', { 'class': 'ndpi-quick-stat' }, [
E('div', { 'class': 'ndpi-quick-stat-header' }, [
@@ -121,109 +231,197 @@ return view.extend({
E('span', { 'class': 'ndpi-quick-stat-label' }, 'Total Flows')
]),
E('div', { 'class': 'ndpi-quick-stat-value ndpi-flows-total' },
- api.formatNumber(flows.flow_count || 0)),
- E('div', { 'class': 'ndpi-quick-stat-sub' }, 'Since service start')
+ api.formatNumber(status.flow_count || 0)),
+ E('div', { 'class': 'ndpi-quick-stat-sub' }, 'Since start')
]),
E('div', { 'class': 'ndpi-quick-stat' }, [
E('div', { 'class': 'ndpi-quick-stat-header' }, [
- E('span', { 'class': 'ndpi-quick-stat-icon' }, '๐ฆ'),
- E('span', { 'class': 'ndpi-quick-stat-label' }, 'Total Packets')
+ E('span', { 'class': 'ndpi-quick-stat-icon' }, '๐ฑ'),
+ E('span', { 'class': 'ndpi-quick-stat-label' }, 'Applications')
]),
E('div', { 'class': 'ndpi-quick-stat-value' },
- api.formatNumber(totalPackets)),
- E('div', { 'class': 'ndpi-quick-stat-sub' }, 'TCP + UDP + ICMP')
+ api.formatNumber(applications.length)),
+ E('div', { 'class': 'ndpi-quick-stat-sub' }, 'Detected')
]),
E('div', { 'class': 'ndpi-quick-stat' }, [
E('div', { 'class': 'ndpi-quick-stat-header' }, [
- E('span', { 'class': 'ndpi-quick-stat-icon' }, 'โฑ'),
- E('span', { 'class': 'ndpi-quick-stat-label' }, 'Uptime')
+ E('span', { 'class': 'ndpi-quick-stat-icon' }, '๐ท๏ธ'),
+ E('span', { 'class': 'ndpi-quick-stat-label' }, 'Categories')
]),
E('div', { 'class': 'ndpi-quick-stat-value' },
- api.formatUptime(flows.uptime || 0)),
- E('div', { 'class': 'ndpi-quick-stat-sub' }, 'Service runtime')
+ api.formatNumber(categories.length)),
+ E('div', { 'class': 'ndpi-quick-stat-sub' }, 'Traffic types')
])
]),
- // Interface Statistics Table
- E('div', { 'class': 'ndpi-card' }, [
- E('div', { 'class': 'ndpi-card-header' }, [
- E('div', { 'class': 'ndpi-card-title' }, [
- E('span', { 'class': 'ndpi-card-title-icon' }, '๐'),
- 'Per-Interface Statistics'
+ // Main content grid
+ E('div', { 'class': 'ndpi-grid-2' }, [
+ // Flows Table
+ E('div', { 'class': 'ndpi-card ndpi-card-wide' }, [
+ E('div', { 'class': 'ndpi-card-header' }, [
+ E('div', { 'class': 'ndpi-card-title' }, [
+ E('span', { 'class': 'ndpi-card-title-icon' }, '๐'),
+ 'Live Flow Detection'
+ ]),
+ E('div', { 'class': 'ndpi-card-badge' }, flows.length + ' flows')
]),
- E('div', { 'class': 'ndpi-card-badge' },
- interfaces.length + ' interface' + (interfaces.length !== 1 ? 's' : ''))
- ]),
- E('div', { 'class': 'ndpi-card-body' },
- interfaces.length > 0 ?
- E('div', { 'class': 'ndpi-table-container' }, [
- E('table', { 'class': 'ndpi-table' }, [
+ E('div', { 'class': 'ndpi-card-body ndpi-flows-table-container' },
+ flows.length > 0 ?
+ E('table', { 'class': 'ndpi-table ndpi-flows-table' }, [
E('thead', {}, [
E('tr', {}, [
- E('th', {}, 'Interface'),
- E('th', {}, 'TCP'),
- E('th', {}, 'UDP'),
- E('th', {}, 'ICMP'),
- E('th', {}, 'Total Bytes')
+ E('th', {}, 'Application'),
+ E('th', {}, 'Source'),
+ E('th', {}, ''),
+ E('th', {}, 'Destination'),
+ E('th', {}, 'Proto'),
+ E('th', {}, 'Category'),
+ E('th', {}, 'Traffic'),
+ E('th', {}, '')
])
]),
- E('tbody', {},
- interfaces.map(function(iface) {
- return E('tr', { 'class': 'ndpi-iface-row', 'data-iface': iface.name }, [
- E('td', {}, [
- E('span', { 'class': 'ndpi-app-name' }, iface.name)
+ E('tbody', { 'class': 'ndpi-flows-tbody' },
+ flows.slice(0, 50).map(function(flow) {
+ return E('tr', { 'class': 'ndpi-flow-row ' + (flow.state === 'ended' ? 'ndpi-flow-ended' : 'ndpi-flow-active') }, [
+ E('td', { 'class': 'ndpi-flow-app' }, [
+ E('span', { 'class': 'ndpi-app-icon' }, self.getAppIcon(flow.app, flow.category)),
+ E('div', { 'class': 'ndpi-app-info' }, [
+ E('span', { 'class': 'ndpi-app-name' }, flow.app || 'Unknown'),
+ E('span', { 'class': 'ndpi-app-host' }, flow.hostname || '')
+ ])
]),
- E('td', { 'class': 'mono ndpi-iface-tcp' }, api.formatNumber(iface.tcp)),
- E('td', { 'class': 'mono ndpi-iface-udp' }, api.formatNumber(iface.udp)),
- E('td', { 'class': 'mono ndpi-iface-icmp' }, api.formatNumber(iface.icmp)),
- E('td', { 'class': 'mono ndpi-iface-bytes' }, api.formatBytes(iface.ip_bytes))
+ E('td', { 'class': 'ndpi-flow-src mono' }, flow.src_ip + ':' + flow.src_port),
+ E('td', { 'class': 'ndpi-flow-arrow' }, 'โ'),
+ E('td', { 'class': 'ndpi-flow-dst mono' }, flow.dst_ip + ':' + flow.dst_port),
+ E('td', { 'class': 'ndpi-flow-proto' }, [
+ E('span', { 'class': 'ndpi-proto-badge ndpi-proto-' + self.getProtoName(flow.proto).toLowerCase() }, self.getProtoName(flow.proto))
+ ]),
+ E('td', { 'class': 'ndpi-flow-category' }, [
+ E('span', { 'class': 'ndpi-category-badge', 'style': 'background:' + self.getCategoryColor(flow.category) }, flow.category || 'Unknown')
+ ]),
+ E('td', { 'class': 'ndpi-flow-bytes mono' }, api.formatBytes((flow.bytes_rx || 0) + (flow.bytes_tx || 0))),
+ E('td', { 'class': 'ndpi-flow-state' }, [
+ E('span', { 'class': 'ndpi-state-' + flow.state }, flow.state === 'active' ? 'โ' : 'โ')
+ ])
]);
})
)
+ ]) :
+ E('div', { 'class': 'ndpi-empty' }, [
+ E('div', { 'class': 'ndpi-empty-icon' }, '๐'),
+ E('div', { 'class': 'ndpi-empty-text' }, 'No flows detected yet'),
+ E('div', { 'class': 'ndpi-empty-hint' }, 'Generate some network traffic to see detection')
])
- ]) :
- E('div', { 'class': 'ndpi-empty' }, [
- E('div', { 'class': 'ndpi-empty-icon' }, '๐'),
- E('div', { 'class': 'ndpi-empty-text' }, 'No interface statistics available')
- ])
- )
+ )
+ ]),
+
+ // Top Applications
+ E('div', { 'class': 'ndpi-card' }, [
+ E('div', { 'class': 'ndpi-card-header' }, [
+ E('div', { 'class': 'ndpi-card-title' }, [
+ E('span', { 'class': 'ndpi-card-title-icon' }, '๐ฑ'),
+ 'Top Applications'
+ ])
+ ]),
+ E('div', { 'class': 'ndpi-card-body' },
+ applications.length > 0 ?
+ E('div', { 'class': 'ndpi-apps-list' },
+ (function() {
+ var maxBytes = Math.max.apply(null, applications.map(function(a) { return a.bytes || 0; })) || 1;
+ return applications.slice(0, 10).map(function(app) {
+ var pct = Math.round(((app.bytes || 0) / maxBytes) * 100);
+ return E('div', { 'class': 'ndpi-app-item' }, [
+ E('div', { 'class': 'ndpi-app-header' }, [
+ E('span', { 'class': 'ndpi-app-icon' }, self.getAppIcon(app.name, app.category)),
+ E('span', { 'class': 'ndpi-app-name' }, app.name),
+ E('span', { 'class': 'ndpi-app-bytes' }, api.formatBytes(app.bytes || 0))
+ ]),
+ E('div', { 'class': 'ndpi-app-bar' }, [
+ E('div', { 'class': 'ndpi-app-bar-fill', 'style': 'width:' + pct + '%;background:' + self.getCategoryColor(app.category) })
+ ]),
+ E('div', { 'class': 'ndpi-app-meta' }, (app.flows || 0) + ' flows ยท ' + (app.category || 'Unknown'))
+ ]);
+ });
+ })()
+ ) :
+ E('div', { 'class': 'ndpi-empty' }, [
+ E('div', { 'class': 'ndpi-empty-icon' }, '๐ฑ'),
+ E('div', { 'class': 'ndpi-empty-text' }, 'No applications detected yet')
+ ])
+ )
+ ])
]),
- // Protocol Breakdown
- E('div', { 'class': 'ndpi-card' }, [
- E('div', { 'class': 'ndpi-card-header' }, [
- E('div', { 'class': 'ndpi-card-title' }, [
- E('span', { 'class': 'ndpi-card-title-icon' }, '๐ก'),
- 'Protocol Breakdown'
- ])
+ // Protocol & Category breakdown
+ E('div', { 'class': 'ndpi-grid-2' }, [
+ // Protocol Distribution
+ E('div', { 'class': 'ndpi-card' }, [
+ E('div', { 'class': 'ndpi-card-header' }, [
+ E('div', { 'class': 'ndpi-card-title' }, [
+ E('span', { 'class': 'ndpi-card-title-icon' }, '๐ก'),
+ 'Protocol Distribution'
+ ])
+ ]),
+ E('div', { 'class': 'ndpi-card-body' },
+ protocols.length > 0 ?
+ E('div', { 'class': 'ndpi-protocol-grid' },
+ protocols.map(function(proto) {
+ var pct = totalPackets > 0 ? Math.round((proto.count / totalPackets) * 100) : 0;
+ var color = proto.name === 'TCP' ? '#0ea5e9' :
+ proto.name === 'UDP' ? '#10b981' : '#f59e0b';
+ return E('div', { 'class': 'ndpi-protocol-item' }, [
+ E('div', { 'class': 'ndpi-protocol-header' }, [
+ E('span', { 'class': 'ndpi-protocol-name' }, proto.name),
+ E('span', { 'class': 'ndpi-protocol-count' }, api.formatNumber(proto.count))
+ ]),
+ E('div', { 'class': 'ndpi-protocol-bar' }, [
+ E('div', { 'class': 'ndpi-protocol-bar-fill', 'style': 'width:' + pct + '%;background:' + color })
+ ]),
+ E('div', { 'class': 'ndpi-protocol-pct' }, pct + '%')
+ ]);
+ })
+ ) :
+ E('div', { 'class': 'ndpi-empty' }, [
+ E('div', { 'class': 'ndpi-empty-icon' }, '๐ก'),
+ E('div', { 'class': 'ndpi-empty-text' }, 'No protocol data')
+ ])
+ )
]),
- E('div', { 'class': 'ndpi-card-body' },
- protocols.length > 0 ?
- E('div', { 'class': 'ndpi-protocol-grid' },
- protocols.map(function(proto) {
- var pct = totalPackets > 0 ? Math.round((proto.count / totalPackets) * 100) : 0;
- var color = proto.name === 'TCP' ? '#0ea5e9' :
- proto.name === 'UDP' ? '#10b981' : '#f59e0b';
- return E('div', { 'class': 'ndpi-protocol-item' }, [
- E('div', { 'class': 'ndpi-protocol-header' }, [
- E('span', { 'class': 'ndpi-protocol-name' }, proto.name),
- E('span', { 'class': 'ndpi-protocol-count' }, api.formatNumber(proto.count))
- ]),
- E('div', { 'class': 'ndpi-protocol-bar' }, [
- E('div', {
- 'class': 'ndpi-protocol-bar-fill',
- 'style': 'width: ' + pct + '%; background: ' + color
- })
- ]),
- E('div', { 'class': 'ndpi-protocol-pct' }, pct + '%')
- ]);
- })
- ) :
- E('div', { 'class': 'ndpi-empty' }, [
- E('div', { 'class': 'ndpi-empty-icon' }, '๐ก'),
- E('div', { 'class': 'ndpi-empty-text' }, 'No protocol data available')
- ])
- )
+
+ // Categories
+ E('div', { 'class': 'ndpi-card' }, [
+ E('div', { 'class': 'ndpi-card-header' }, [
+ E('div', { 'class': 'ndpi-card-title' }, [
+ E('span', { 'class': 'ndpi-card-title-icon' }, '๐ท๏ธ'),
+ 'Traffic Categories'
+ ])
+ ]),
+ E('div', { 'class': 'ndpi-card-body' },
+ categories.length > 0 ?
+ E('div', { 'class': 'ndpi-categories-list' },
+ (function() {
+ var maxBytes = Math.max.apply(null, categories.map(function(c) { return c.bytes || 0; })) || 1;
+ return categories.slice(0, 8).map(function(cat) {
+ var pct = Math.round(((cat.bytes || 0) / maxBytes) * 100);
+ return E('div', { 'class': 'ndpi-category-item' }, [
+ E('div', { 'class': 'ndpi-category-header' }, [
+ E('span', { 'class': 'ndpi-category-name', 'style': 'color:' + self.getCategoryColor(cat.name) }, cat.name),
+ E('span', { 'class': 'ndpi-category-bytes' }, api.formatBytes(cat.bytes || 0))
+ ]),
+ E('div', { 'class': 'ndpi-category-bar' }, [
+ E('div', { 'class': 'ndpi-category-bar-fill', 'style': 'width:' + pct + '%;background:' + self.getCategoryColor(cat.name) })
+ ]),
+ E('div', { 'class': 'ndpi-category-meta' }, (cat.apps || 0) + ' apps ยท ' + (cat.flows || 0) + ' flows')
+ ]);
+ });
+ })()
+ ) :
+ E('div', { 'class': 'ndpi-empty' }, [
+ E('div', { 'class': 'ndpi-empty-icon' }, '๐ท๏ธ'),
+ E('div', { 'class': 'ndpi-empty-text' }, 'No categories detected')
+ ])
+ )
+ ])
])
]);
diff --git a/package/secubox/luci-app-ndpid/root/usr/bin/ndpid-collector b/package/secubox/luci-app-ndpid/root/usr/bin/ndpid-collector
old mode 100644
new mode 100755
diff --git a/package/secubox/luci-app-ndpid/root/usr/bin/ndpid-compat b/package/secubox/luci-app-ndpid/root/usr/bin/ndpid-compat
index 636d8002..ed7297f5 100644
--- a/package/secubox/luci-app-ndpid/root/usr/bin/ndpid-compat
+++ b/package/secubox/luci-app-ndpid/root/usr/bin/ndpid-compat
@@ -1,6 +1,6 @@
#!/bin/sh
# nDPId to Netifyd Compatibility Layer
-# Translates nDPId events to Netifyd-compatible format
+# Translates nDPId events to Netifyd-compatible format with enhanced detection
# Copyright (C) 2025 CyberMind.fr
. /lib/functions.sh
@@ -10,16 +10,19 @@
DISTRIBUTOR_SOCK="/var/run/ndpid/distributor.sock"
STATUS_FILE="/var/run/netifyd/status.json"
FLOWS_FILE="/tmp/ndpid-flows.json"
+APPS_FILE="/tmp/ndpid-apps.json"
STATS_FILE="/tmp/ndpid-stats.json"
-STATS_HISTORY="/tmp/ndpid-stats-history.json"
UPDATE_INTERVAL=1
-MAX_HISTORY=1440
+MAX_FLOWS=500
+MAX_APPS=100
-# State variables (stored in temp files for shell compatibility)
+# State directory
STATE_DIR="/tmp/ndpid-state"
FLOWS_ACTIVE_FILE="$STATE_DIR/flows_active"
FLOW_COUNT_FILE="$STATE_DIR/flow_count"
STATS_FILE_TMP="$STATE_DIR/stats"
+FLOWS_TMP="$STATE_DIR/flows"
+APPS_TMP="$STATE_DIR/apps"
# Initialize state
init_state() {
@@ -28,16 +31,17 @@ init_state() {
echo "0" > "$FLOWS_ACTIVE_FILE"
echo "0" > "$FLOW_COUNT_FILE"
echo "{}" > "$STATS_FILE_TMP"
+ echo "[]" > "$FLOWS_TMP"
+ echo "{}" > "$APPS_TMP"
}
-# Increment counter
+# Counter operations
inc_counter() {
local file="$1"
local val=$(cat "$file" 2>/dev/null || echo 0)
echo $((val + 1)) > "$file"
}
-# Decrement counter
dec_counter() {
local file="$1"
local val=$(cat "$file" 2>/dev/null || echo 0)
@@ -45,7 +49,6 @@ dec_counter() {
echo "$val" > "$file"
}
-# Get counter value
get_counter() {
cat "$1" 2>/dev/null || echo 0
}
@@ -56,62 +59,177 @@ update_iface_stats() {
local proto="$2"
local bytes="$3"
+ [ -z "$iface" ] && return
+
local stats=$(cat "$STATS_FILE_TMP" 2>/dev/null || echo "{}")
- # Use jq to update stats (if available) or simple JSON
if command -v jq >/dev/null 2>&1; then
- stats=$(echo "$stats" | jq --arg iface "$iface" --arg proto "$proto" --argjson bytes "$bytes" '
+ stats=$(echo "$stats" | jq --arg iface "$iface" --arg proto "$proto" --argjson bytes "${bytes:-0}" '
.[$iface] //= {"ip_bytes": 0, "wire_bytes": 0, "tcp": 0, "udp": 0, "icmp": 0} |
.[$iface].ip_bytes += $bytes |
.[$iface].wire_bytes += $bytes |
- if $proto == "tcp" then .[$iface].tcp += 1
- elif $proto == "udp" then .[$iface].udp += 1
- elif $proto == "icmp" then .[$iface].icmp += 1
+ if $proto == "tcp" or $proto == "6" then .[$iface].tcp += 1
+ elif $proto == "udp" or $proto == "17" then .[$iface].udp += 1
+ elif $proto == "icmp" or $proto == "1" then .[$iface].icmp += 1
else . end
')
echo "$stats" > "$STATS_FILE_TMP"
fi
}
+# Update application stats
+update_app_stats() {
+ local app="$1"
+ local category="$2"
+ local bytes="$3"
+
+ [ -z "$app" ] && return
+
+ if command -v jq >/dev/null 2>&1; then
+ local apps=$(cat "$APPS_TMP" 2>/dev/null || echo "{}")
+ apps=$(echo "$apps" | jq --arg app "$app" --arg cat "${category:-Unknown}" --argjson bytes "${bytes:-0}" '
+ .[$app] //= {"name": $app, "category": $cat, "flows": 0, "bytes": 0} |
+ .[$app].flows += 1 |
+ .[$app].bytes += $bytes
+ ')
+ echo "$apps" > "$APPS_TMP"
+ fi
+}
+
+# Add flow to list
+add_flow() {
+ local json="$1"
+
+ if command -v jq >/dev/null 2>&1; then
+ local flow_info=$(echo "$json" | jq -c '{
+ id: .flow_id,
+ src_ip: .src_ip,
+ src_port: .src_port,
+ dst_ip: .dst_ip,
+ dst_port: .dst_port,
+ proto: .l4_proto,
+ app: (.ndpi.app_proto // .ndpi.proto // "Unknown"),
+ category: (.ndpi.category // "Unknown"),
+ hostname: (.ndpi.hostname // .flow_dst_hostname // null),
+ confidence: (.ndpi.confidence // "Unknown"),
+ risk: (.ndpi.flow_risk // []),
+ bytes_rx: (.flow_src_tot_l4_payload_len // 0),
+ bytes_tx: (.flow_dst_tot_l4_payload_len // 0),
+ packets: ((.flow_src_packets_processed // 0) + (.flow_dst_packets_processed // 0)),
+ first_seen: .flow_first_seen,
+ last_seen: .flow_last_seen,
+ state: "active",
+ iface: .source
+ }' 2>/dev/null)
+
+ [ -z "$flow_info" ] && return
+
+ local flows=$(cat "$FLOWS_TMP" 2>/dev/null || echo "[]")
+ flows=$(echo "$flows" | jq --argjson flow "$flow_info" --argjson max "$MAX_FLOWS" '
+ [. | .[] | select(.id != $flow.id)] + [$flow] | .[-$max:]
+ ')
+ echo "$flows" > "$FLOWS_TMP"
+ fi
+}
+
+# Update existing flow
+update_flow() {
+ local json="$1"
+ local flow_id=$(echo "$json" | jsonfilter -e '@.flow_id' 2>/dev/null)
+
+ [ -z "$flow_id" ] && return
+
+ if command -v jq >/dev/null 2>&1; then
+ local update_info=$(echo "$json" | jq -c '{
+ bytes_rx: (.flow_src_tot_l4_payload_len // 0),
+ bytes_tx: (.flow_dst_tot_l4_payload_len // 0),
+ packets: ((.flow_src_packets_processed // 0) + (.flow_dst_packets_processed // 0)),
+ last_seen: .flow_last_seen
+ }' 2>/dev/null)
+
+ local flows=$(cat "$FLOWS_TMP" 2>/dev/null || echo "[]")
+ flows=$(echo "$flows" | jq --arg id "$flow_id" --argjson update "$update_info" '
+ map(if .id == ($id | tonumber) then . + $update else . end)
+ ')
+ echo "$flows" > "$FLOWS_TMP"
+ fi
+}
+
+# Mark flow as ended
+end_flow() {
+ local json="$1"
+ local flow_id=$(echo "$json" | jsonfilter -e '@.flow_id' 2>/dev/null)
+
+ [ -z "$flow_id" ] && return
+
+ if command -v jq >/dev/null 2>&1; then
+ local flows=$(cat "$FLOWS_TMP" 2>/dev/null || echo "[]")
+ flows=$(echo "$flows" | jq --arg id "$flow_id" '
+ map(if .id == ($id | tonumber) then .state = "ended" else . end)
+ ')
+ echo "$flows" > "$FLOWS_TMP"
+ fi
+}
+
# Process a single nDPId event
process_event() {
local raw="$1"
- # Strip 5-digit length prefix
- local json="${raw:5}"
+ # Strip 5-digit length prefix if present
+ local json="$raw"
+ if echo "$raw" | grep -q '^[0-9]\{5\}'; then
+ json="${raw:5}"
+ fi
# Parse event type
local event_name=$(echo "$json" | jsonfilter -e '@.flow_event_name' 2>/dev/null)
[ -z "$event_name" ] && event_name=$(echo "$json" | jsonfilter -e '@.daemon_event_name' 2>/dev/null)
+ # Extract common fields
+ local iface=$(echo "$json" | jsonfilter -e '@.source' 2>/dev/null)
+ local proto=$(echo "$json" | jsonfilter -e '@.l4_proto' 2>/dev/null)
+ local src_bytes=$(echo "$json" | jsonfilter -e '@.flow_src_tot_l4_payload_len' 2>/dev/null || echo 0)
+ local dst_bytes=$(echo "$json" | jsonfilter -e '@.flow_dst_tot_l4_payload_len' 2>/dev/null || echo 0)
+ local total_bytes=$((src_bytes + dst_bytes))
+
case "$event_name" in
new)
inc_counter "$FLOW_COUNT_FILE"
inc_counter "$FLOWS_ACTIVE_FILE"
+ add_flow "$json"
+ ;;
+ detected|guessed|detection-update)
+ # Extract application info
+ local app=$(echo "$json" | jsonfilter -e '@.ndpi.app_proto' 2>/dev/null)
+ [ -z "$app" ] && app=$(echo "$json" | jsonfilter -e '@.ndpi.proto' 2>/dev/null)
+ local category=$(echo "$json" | jsonfilter -e '@.ndpi.category' 2>/dev/null)
+
+ # Update stats
+ [ -n "$iface" ] && update_iface_stats "$iface" "$proto" "$total_bytes"
+ [ -n "$app" ] && update_app_stats "$app" "$category" "$total_bytes"
+
+ # Update flow details
+ add_flow "$json"
+ ;;
+ update)
+ update_flow "$json"
+ [ -n "$iface" ] && update_iface_stats "$iface" "$proto" "$total_bytes"
;;
end|idle)
dec_counter "$FLOWS_ACTIVE_FILE"
- ;;
- detected|guessed)
- # Extract flow info for stats
- local iface=$(echo "$json" | jsonfilter -e '@.source' 2>/dev/null)
- local proto=$(echo "$json" | jsonfilter -e '@.l4_proto' 2>/dev/null)
- local src_bytes=$(echo "$json" | jsonfilter -e '@.flow_src_tot_l4_payload_len' 2>/dev/null || echo 0)
- local dst_bytes=$(echo "$json" | jsonfilter -e '@.flow_dst_tot_l4_payload_len' 2>/dev/null || echo 0)
- local total_bytes=$((src_bytes + dst_bytes))
-
- [ -n "$iface" ] && update_iface_stats "$iface" "$proto" "$total_bytes"
+ end_flow "$json"
;;
esac
}
-# Generate Netifyd-compatible status.json
+# Generate status files
generate_status() {
local flow_count=$(get_counter "$FLOW_COUNT_FILE")
local flows_active=$(get_counter "$FLOWS_ACTIVE_FILE")
local stats=$(cat "$STATS_FILE_TMP" 2>/dev/null || echo "{}")
local uptime=$(($(date +%s) - START_TIME))
+ # Generate main status file
if command -v jq >/dev/null 2>&1; then
jq -n \
--argjson flow_count "$flow_count" \
@@ -127,6 +245,13 @@ generate_status() {
uptime: $uptime,
source: "ndpid-compat"
}' > "$STATUS_FILE"
+
+ # Generate flows file
+ cp "$FLOWS_TMP" "$FLOWS_FILE" 2>/dev/null
+
+ # Generate apps file (sorted by bytes)
+ local apps=$(cat "$APPS_TMP" 2>/dev/null || echo "{}")
+ echo "$apps" | jq '[.[] | select(.name != null)] | sort_by(-.bytes) | .[0:100]' > "$APPS_FILE" 2>/dev/null
else
cat > "$STATUS_FILE" << EOF
{
@@ -142,21 +267,38 @@ EOF
fi
}
+# Cleanup old ended flows
+cleanup_flows() {
+ if command -v jq >/dev/null 2>&1; then
+ local flows=$(cat "$FLOWS_TMP" 2>/dev/null || echo "[]")
+ local now=$(date +%s)
+ # Keep active flows and ended flows from last 5 minutes
+ flows=$(echo "$flows" | jq --argjson now "$now" '
+ [.[] | select(.state == "active" or (.last_seen != null and ($now - .last_seen) < 300))]
+ ')
+ echo "$flows" > "$FLOWS_TMP"
+ fi
+}
+
# Main loop
main() {
START_TIME=$(date +%s)
- logger -t ndpid-compat "Starting nDPId compatibility layer"
+ logger -t ndpid-compat "Starting nDPId compatibility layer (enhanced)"
# Initialize state
init_state
- # Check for socat
+ # Check for dependencies
if ! command -v socat >/dev/null 2>&1; then
- logger -t ndpid-compat "ERROR: socat not found, using nc fallback"
+ logger -t ndpid-compat "WARNING: socat not found, using nc fallback"
USE_NC=1
fi
+ if ! command -v jq >/dev/null 2>&1; then
+ logger -t ndpid-compat "WARNING: jq not found, detailed stats disabled"
+ fi
+
# Wait for distributor socket
local wait_count=0
while [ ! -S "$DISTRIBUTOR_SOCK" ] && [ $wait_count -lt 30 ]; do
@@ -179,7 +321,17 @@ main() {
done
) &
STATUS_PID=$!
- trap "kill $STATUS_PID 2>/dev/null" EXIT
+
+ # Background cleanup
+ (
+ while true; do
+ sleep 60
+ cleanup_flows
+ done
+ ) &
+ CLEANUP_PID=$!
+
+ trap "kill $STATUS_PID $CLEANUP_PID 2>/dev/null" EXIT
# Read events from distributor
if [ -z "$USE_NC" ]; then
@@ -193,11 +345,12 @@ main() {
fi
}
-# Run main if not sourced
+# Run modes
case "$1" in
-h|--help)
echo "Usage: $0 [-d|--daemon]"
- echo " Translates nDPId events to Netifyd-compatible format"
+ echo " Enhanced nDPId to Netifyd compatibility layer"
+ echo " Captures detailed flow and application information"
exit 0
;;
-d|--daemon)
diff --git a/package/secubox/luci-app-ndpid/root/usr/bin/ndpid-flow-actions b/package/secubox/luci-app-ndpid/root/usr/bin/ndpid-flow-actions
old mode 100644
new mode 100755
diff --git a/package/secubox/luci-app-ndpid/root/usr/libexec/rpcd/luci.ndpid b/package/secubox/luci-app-ndpid/root/usr/libexec/rpcd/luci.ndpid
index cddcfc48..38f02f79 100755
--- a/package/secubox/luci-app-ndpid/root/usr/libexec/rpcd/luci.ndpid
+++ b/package/secubox/luci-app-ndpid/root/usr/libexec/rpcd/luci.ndpid
@@ -13,6 +13,7 @@ STATUS_FILE="/var/run/netifyd/status.json"
DISTRIBUTOR_SOCK="/var/run/ndpid/distributor.sock"
COLLECTOR_SOCK="/var/run/ndpid/collector.sock"
FLOWS_CACHE="/tmp/ndpid-flows.json"
+APPS_CACHE="/tmp/ndpid-apps.json"
STATS_CACHE="/tmp/ndpid-stats.json"
LOG_FILE="/var/log/ndpid.log"
@@ -138,31 +139,124 @@ get_interface_stats() {
json_dump
}
-# Get top applications (from flow analysis)
+# Get top applications (from aggregated apps file)
get_top_applications() {
json_init
json_add_array "applications"
- # Read from flows cache if available
- if [ -f "$FLOWS_CACHE" ] && command -v jq >/dev/null 2>&1; then
- # Aggregate by application
- jq -r '
- group_by(.application) |
+ local app_data=""
+
+ # Try apps cache first (pre-aggregated by ndpid-compat)
+ if [ -f "$APPS_CACHE" ] && command -v jq >/dev/null 2>&1; then
+ app_data=$(jq -r '.[0:15][] | "\(.name // "Unknown")|\(.category // "Unknown")|\(.flows // 0)|\(.bytes // 0)"' \
+ "$APPS_CACHE" 2>/dev/null)
+ # Fallback to flows cache
+ elif [ -f "$FLOWS_CACHE" ] && command -v jq >/dev/null 2>&1; then
+ app_data=$(jq -r '
+ group_by(.app) |
map({
- name: .[0].application,
+ name: (.[0].app // "Unknown"),
+ category: (.[0].category // "Unknown"),
flows: length,
- bytes: (map(.bytes_rx + .bytes_tx) | add)
+ bytes: ([.[].bytes_rx, .[].bytes_tx] | add // 0)
}) |
sort_by(-.bytes) |
- .[0:10][] |
- "\(.name)|\(.flows)|\(.bytes)"
- ' "$FLOWS_CACHE" 2>/dev/null | while IFS='|' read -r name flows bytes; do
+ .[0:15][] |
+ "\(.name)|\(.category)|\(.flows)|\(.bytes)"
+ ' "$FLOWS_CACHE" 2>/dev/null)
+ fi
+
+ if [ -n "$app_data" ]; then
+ while IFS='|' read -r name category flows bytes; do
+ [ -z "$name" ] && continue
json_add_object
- json_add_string "name" "${name:-unknown}"
+ json_add_string "name" "${name:-Unknown}"
+ json_add_string "category" "${category:-Unknown}"
json_add_int "flows" "${flows:-0}"
json_add_int "bytes" "${bytes:-0}"
json_close_object
- done
+ done </dev/null 2>&1; then
+ # Get recent active flows sorted by bytes - use heredoc to avoid subshell
+ local flow_data
+ flow_data=$(jq -r '
+ sort_by(-(.bytes_rx + .bytes_tx)) |
+ .[0:100][] |
+ "\(.id // 0)|\(.src_ip // "")|\(.src_port // 0)|\(.dst_ip // "")|\(.dst_port // 0)|\(.proto // "")|\(.app // "Unknown")|\(.category // "Unknown")|\(.hostname // "")|\(.confidence // "")|\(.bytes_rx // 0)|\(.bytes_tx // 0)|\(.packets // 0)|\(.state // "unknown")|\(.iface // "")"
+ ' "$FLOWS_CACHE" 2>/dev/null)
+
+ while IFS='|' read -r id src_ip src_port dst_ip dst_port proto app category hostname confidence bytes_rx bytes_tx packets state iface; do
+ [ -z "$id" ] && continue
+ json_add_object
+ json_add_int "id" "${id:-0}"
+ json_add_string "src_ip" "${src_ip}"
+ json_add_int "src_port" "${src_port:-0}"
+ json_add_string "dst_ip" "${dst_ip}"
+ json_add_int "dst_port" "${dst_port:-0}"
+ json_add_string "proto" "${proto}"
+ json_add_string "app" "${app:-Unknown}"
+ json_add_string "category" "${category:-Unknown}"
+ json_add_string "hostname" "${hostname}"
+ json_add_string "confidence" "${confidence}"
+ json_add_int "bytes_rx" "${bytes_rx:-0}"
+ json_add_int "bytes_tx" "${bytes_tx:-0}"
+ json_add_int "packets" "${packets:-0}"
+ json_add_string "state" "${state:-unknown}"
+ json_add_string "iface" "${iface}"
+ json_close_object
+ done </dev/null 2>&1; then
+ local cat_data
+ cat_data=$(jq -r '
+ group_by(.category) |
+ map({
+ name: (.[0].category // "Unknown"),
+ apps: length,
+ flows: ([.[].flows] | add),
+ bytes: ([.[].bytes] | add)
+ }) |
+ sort_by(-.bytes) |
+ .[] |
+ "\(.name)|\(.apps)|\(.flows)|\(.bytes)"
+ ' "$APPS_CACHE" 2>/dev/null)
+
+ while IFS='|' read -r name apps flows bytes; do
+ [ -z "$name" ] && continue
+ json_add_object
+ json_add_string "name" "${name:-Unknown}"
+ json_add_int "apps" "${apps:-0}"
+ json_add_int "flows" "${flows:-0}"
+ json_add_int "bytes" "${bytes:-0}"
+ json_close_object
+ done </dev/null | grep -E '^(br-|eth|wlan)'); do
- json_add_string "" "$iface"
+
+ # Get bridges and network interfaces from UCI
+ local net_ifaces=""
+ config_load network
+
+ # Get all defined interfaces (br-lan, br-wan, etc)
+ for iface in $(ls /sys/class/net/ 2>/dev/null); do
+ case "$iface" in
+ lo|sit*|ip6*|gre*|ifb*|teql*)
+ # Skip loopback and virtual tunnel interfaces
+ ;;
+ br-*|eth*|wlan*|lan*|wan*)
+ # Include bridges, ethernet, wifi, and named interfaces
+ local state=$(cat /sys/class/net/$iface/operstate 2>/dev/null || echo "unknown")
+ local type=$(cat /sys/class/net/$iface/type 2>/dev/null || echo "0")
+ # Only include if it's a real interface (type 1 = ethernet)
+ if [ "$type" = "1" ] || [ -d "/sys/class/net/$iface/bridge" ]; then
+ json_add_object
+ json_add_string "name" "$iface"
+ json_add_string "state" "$state"
+ # Check if it's a bridge
+ if [ -d "/sys/class/net/$iface/bridge" ]; then
+ json_add_string "type" "bridge"
+ else
+ json_add_string "type" "interface"
+ fi
+ json_close_object
+ fi
+ ;;
+ esac
done
+
json_close_array
json_dump
@@ -522,9 +645,11 @@ case "$1" in
{
"get_service_status": {},
"get_realtime_flows": {},
+ "get_detailed_flows": {},
"get_interface_stats": {},
"get_top_applications": {},
"get_top_protocols": {},
+ "get_categories": {},
"get_config": {},
"get_dashboard": {},
"get_interfaces": {},
@@ -546,6 +671,9 @@ EOF
get_realtime_flows)
get_realtime_flows
;;
+ get_detailed_flows)
+ get_detailed_flows
+ ;;
get_interface_stats)
get_interface_stats
;;
@@ -555,6 +683,9 @@ EOF
get_top_protocols)
get_top_protocols
;;
+ get_categories)
+ get_categories
+ ;;
get_config)
get_config
;;
diff --git a/package/secubox/luci-app-ndpid/root/usr/share/rpcd/acl.d/luci-app-ndpid.json b/package/secubox/luci-app-ndpid/root/usr/share/rpcd/acl.d/luci-app-ndpid.json
index a81f2879..fea3f495 100644
--- a/package/secubox/luci-app-ndpid/root/usr/share/rpcd/acl.d/luci-app-ndpid.json
+++ b/package/secubox/luci-app-ndpid/root/usr/share/rpcd/acl.d/luci-app-ndpid.json
@@ -6,9 +6,11 @@
"luci.ndpid": [
"get_service_status",
"get_realtime_flows",
+ "get_detailed_flows",
"get_interface_stats",
"get_top_applications",
"get_top_protocols",
+ "get_categories",
"get_config",
"get_dashboard",
"get_interfaces"