secubox-openwrt/package/secubox/luci-app-ndpid/htdocs/luci-static/resources/view/ndpid/flows.js
CyberMind-FR 9048f6b53b style(ndpid): Migrate dashboard and flows views to KISS theme
- Remove old secubox-theme and secubox-portal/header dependencies
- Remove external dashboard.css stylesheet
- Replace ndpid/api with direct RPC declarations
- Use KISS classes (kiss-card, kiss-stat, kiss-table, kiss-badge, kiss-btn)
- Add consistent navigation tabs
- Add poll toggle for auto-refresh control
- Use CSS variables (--kiss-blue, --kiss-green, --kiss-muted, etc.)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-12 13:51:36 +01:00

398 lines
14 KiB
JavaScript

'use strict';
'require view';
'require poll';
'require dom';
'require ui';
'require rpc';
'require secubox/kiss-theme';
var callGetFlows = rpc.declare({
object: 'luci.ndpid',
method: 'flows',
expect: {}
});
var callGetApplications = rpc.declare({
object: 'luci.ndpid',
method: 'applications',
expect: {}
});
var callGetCategories = rpc.declare({
object: 'luci.ndpid',
method: 'categories',
expect: {}
});
var callGetProtocols = rpc.declare({
object: 'luci.ndpid',
method: 'protocols',
expect: {}
});
function formatNumber(n) {
if (!n && n !== 0) return '0';
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
return String(n);
}
function formatBytes(bytes) {
if (!bytes || bytes === 0) return '0 B';
var units = ['B', 'KB', 'MB', 'GB', 'TB'];
var i = Math.floor(Math.log(bytes) / Math.log(1024));
i = Math.min(i, units.length - 1);
return (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + units[i];
}
function getProtoName(proto) {
var protos = { '6': 'TCP', '17': 'UDP', '1': 'ICMP', 'tcp': 'TCP', 'udp': 'UDP', 'icmp': 'ICMP' };
return protos[proto] || proto || '?';
}
function getAppIcon(app, category) {
var icons = {
'HTTP': '🌐', 'HTTPS': '🔒', 'TLS': '🔒', 'SSL': '🔒',
'DNS': '📡', 'NTP': '🕐', 'DHCP': '📋',
'SSH': '🖥️', 'Telnet': '💻',
'YouTube': '▶️', 'Netflix': '🎬', 'Twitch': '🎮',
'Facebook': '👤', 'Twitter': '🐦', 'Instagram': '📷', 'TikTok': '🎵',
'WhatsApp': '💬', 'Telegram': '✈️', 'Discord': '🎧',
'BitTorrent': '📥', 'Spotify': '🎵',
'Zoom': '📹', 'Teams': '👥', 'Skype': '📞',
'VPN': '🛡️', 'OpenVPN': '🛡️', 'WireGuard': '🛡️',
'QUIC': '⚡', 'Unknown': '❓'
};
return icons[app] || icons[category] || '📦';
}
function getCategoryColor(category) {
var colors = {
'Web': 'var(--kiss-blue)',
'Video': 'var(--kiss-red)',
'Streaming': 'var(--kiss-yellow)',
'SocialNetwork': '#ec4899',
'Chat': '#8b5cf6',
'VoIP': 'var(--kiss-green)',
'Game': '#06b6d4',
'Download': '#f97316',
'Cloud': '#6366f1',
'VPN': '#14b8a6',
'Mail': '#84cc16',
'Network': 'var(--kiss-muted)',
'Unknown': 'var(--kiss-muted)'
};
return colors[category] || 'var(--kiss-muted)';
}
return view.extend({
pollInterval: 3,
pollActive: true,
load: function() {
return Promise.all([
callGetFlows(),
callGetApplications(),
callGetCategories(),
callGetProtocols()
]).catch(function() {
return [{}, {}, {}, {}];
});
},
render: function(data) {
var self = this;
var flowsData = data[0] || {};
var applications = data[1].applications || data[1] || [];
var categories = data[2].categories || data[2] || [];
var protocols = data[3].protocols || data[3] || [];
if (!Array.isArray(applications)) applications = [];
if (!Array.isArray(categories)) categories = [];
if (!Array.isArray(protocols)) protocols = [];
var flows = flowsData.flows || [];
var activeFlows = flowsData.active || 0;
var totalFlows = flowsData.total || flows.length;
var content = [
// Header
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px;' }, [
E('div', {}, [
E('h2', { 'style': 'margin: 0 0 4px 0;' }, '🔍 Live Flow Detection'),
E('div', { 'style': 'color: var(--kiss-muted);' }, 'nDPId Deep Packet Inspection')
]),
E('div', { 'style': 'display: flex; gap: 8px; align-items: center;' }, [
E('span', { 'style': 'color: var(--kiss-muted); font-size: 12px;' }, [
'Auto-refresh: ',
E('span', { 'id': 'poll-state', 'style': 'color: var(--kiss-green);' }, 'Active')
]),
E('button', {
'class': 'kiss-btn',
'id': 'poll-toggle',
'click': L.bind(this.togglePoll, this)
}, '⏸ Pause')
])
]),
// Navigation
E('div', { 'class': 'kiss-grid kiss-grid-auto', 'style': 'margin-bottom: 24px;' }, [
E('a', { 'href': L.url('admin', 'secubox', 'ndpid', 'dashboard'), 'class': 'kiss-btn', 'style': 'text-decoration: none;' }, '📊 Dashboard'),
E('a', { 'href': L.url('admin', 'secubox', 'ndpid', 'flows'), 'class': 'kiss-btn kiss-btn-green', 'style': 'text-decoration: none;' }, '🔍 Flows'),
E('a', { 'href': L.url('admin', 'secubox', 'ndpid', 'settings'), 'class': 'kiss-btn', 'style': 'text-decoration: none;' }, '⚙️ Settings')
]),
// Stats
E('div', { 'class': 'kiss-grid kiss-grid-4', 'style': 'margin-bottom: 24px;' }, [
E('div', { 'class': 'kiss-stat' }, [
E('div', { 'class': 'kiss-stat-value', 'style': 'color: var(--kiss-green);', 'data-stat': 'active-flows' }, formatNumber(activeFlows)),
E('div', { 'class': 'kiss-stat-label' }, 'Active Flows')
]),
E('div', { 'class': 'kiss-stat' }, [
E('div', { 'class': 'kiss-stat-value', 'data-stat': 'total-flows' }, formatNumber(totalFlows)),
E('div', { 'class': 'kiss-stat-label' }, 'Total Flows')
]),
E('div', { 'class': 'kiss-stat' }, [
E('div', { 'class': 'kiss-stat-value', 'style': 'color: var(--kiss-blue);' }, applications.length),
E('div', { 'class': 'kiss-stat-label' }, 'Applications')
]),
E('div', { 'class': 'kiss-stat' }, [
E('div', { 'class': 'kiss-stat-value' }, categories.length),
E('div', { 'class': 'kiss-stat-label' }, 'Categories')
])
]),
// Flows Table
E('div', { 'class': 'kiss-card', 'style': 'margin-bottom: 24px;' }, [
E('div', { 'class': 'kiss-card-title' }, [
'Live Flows ',
E('span', { 'class': 'kiss-badge kiss-badge-blue', 'data-stat': 'flows-count' }, flows.length + ' detected')
]),
flows.length > 0 ?
E('div', { 'style': 'overflow-x: auto;' }, [
E('table', { 'class': 'kiss-table', 'id': 'flows-table' }, [
E('tr', {}, [
E('th', {}, 'Application'),
E('th', {}, 'Source'),
E('th', {}, ''),
E('th', {}, 'Destination'),
E('th', {}, 'Proto'),
E('th', {}, 'Category'),
E('th', {}, 'Traffic'),
E('th', {}, '')
])
].concat(flows.slice(0, 50).map(function(flow) {
return self.renderFlowRow(flow);
})))
]) :
E('div', { 'style': 'text-align: center; padding: 40px; color: var(--kiss-muted);' }, [
E('div', { 'style': 'font-size: 32px; margin-bottom: 12px;' }, '🔍'),
E('div', {}, 'No flows detected yet'),
E('div', { 'style': 'font-size: 12px; margin-top: 8px;' }, 'Generate network traffic to see detection')
])
]),
// Two columns
E('div', { 'style': 'display: grid; grid-template-columns: 1fr 1fr; gap: 24px;' }, [
// Top Applications
E('div', { 'class': 'kiss-card' }, [
E('div', { 'class': 'kiss-card-title' }, '📱 Top Applications'),
applications.length > 0 ?
E('div', { 'id': 'apps-list' }, this.renderAppsList(applications)) :
E('div', { 'style': 'text-align: center; padding: 40px; color: var(--kiss-muted);' }, 'No applications detected')
]),
// Traffic Categories
E('div', { 'class': 'kiss-card' }, [
E('div', { 'class': 'kiss-card-title' }, '🏷️ Traffic Categories'),
categories.length > 0 ?
E('div', { 'id': 'categories-list' }, this.renderCategoriesList(categories)) :
E('div', { 'style': 'text-align: center; padding: 40px; color: var(--kiss-muted);' }, 'No categories detected')
])
]),
// Protocol Distribution
protocols.length > 0 ? E('div', { 'class': 'kiss-card', 'style': 'margin-top: 24px;' }, [
E('div', { 'class': 'kiss-card-title' }, '📡 Protocol Distribution'),
E('div', { 'class': 'kiss-grid kiss-grid-auto', 'id': 'protocols-grid' },
this.renderProtocolsList(protocols)
)
]) : E('span')
];
this.startPolling();
return KissTheme.wrap(content, 'ndpid/flows');
},
renderFlowRow: function(flow) {
var stateColor = flow.state === 'active' ? 'var(--kiss-green)' : 'var(--kiss-muted)';
return E('tr', { 'style': flow.state === 'ended' ? 'opacity: 0.6;' : '' }, [
E('td', {}, [
E('span', { 'style': 'margin-right: 6px;' }, getAppIcon(flow.app, flow.category)),
E('span', {}, [
flow.app || 'Unknown',
flow.hostname ? E('span', { 'style': 'color: var(--kiss-muted); font-size: 11px; margin-left: 6px;' }, flow.hostname) : E('span')
])
]),
E('td', { 'style': 'font-family: monospace; font-size: 12px;' }, flow.src_ip + ':' + flow.src_port),
E('td', { 'style': 'color: var(--kiss-muted);' }, '→'),
E('td', { 'style': 'font-family: monospace; font-size: 12px;' }, flow.dst_ip + ':' + flow.dst_port),
E('td', {}, E('span', { 'class': 'kiss-badge' }, getProtoName(flow.proto))),
E('td', {}, E('span', {
'class': 'kiss-badge',
'style': 'background: ' + getCategoryColor(flow.category) + '; color: white;'
}, flow.category || 'Unknown')),
E('td', { 'style': 'font-family: monospace; font-size: 12px;' }, formatBytes((flow.bytes_rx || 0) + (flow.bytes_tx || 0))),
E('td', { 'style': 'color: ' + stateColor + '; font-size: 16px;' }, flow.state === 'active' ? '●' : '○')
]);
},
renderAppsList: function(applications) {
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);
var color = getCategoryColor(app.category);
return E('div', { 'style': 'margin-bottom: 12px;' }, [
E('div', { 'style': 'display: flex; justify-content: space-between; margin-bottom: 4px;' }, [
E('span', {}, [
getAppIcon(app.name, app.category),
' ',
app.name || 'Unknown'
]),
E('span', { 'style': 'color: var(--kiss-muted); font-size: 12px;' }, formatBytes(app.bytes || 0))
]),
E('div', { 'style': 'height: 6px; background: var(--kiss-line); border-radius: 3px; overflow: hidden;' }, [
E('div', { 'style': 'height: 100%; width: ' + pct + '%; background: ' + color + '; border-radius: 3px;' })
]),
E('div', { 'style': 'font-size: 11px; color: var(--kiss-muted); margin-top: 2px;' },
(app.flows || 0) + ' flows · ' + (app.category || 'Unknown'))
]);
});
},
renderCategoriesList: function(categories) {
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);
var color = getCategoryColor(cat.name);
return E('div', { 'style': 'margin-bottom: 12px;' }, [
E('div', { 'style': 'display: flex; justify-content: space-between; margin-bottom: 4px;' }, [
E('span', { 'style': 'color: ' + color + ';' }, cat.name),
E('span', { 'style': 'color: var(--kiss-muted); font-size: 12px;' }, formatBytes(cat.bytes || 0))
]),
E('div', { 'style': 'height: 6px; background: var(--kiss-line); border-radius: 3px; overflow: hidden;' }, [
E('div', { 'style': 'height: 100%; width: ' + pct + '%; background: ' + color + '; border-radius: 3px;' })
]),
E('div', { 'style': 'font-size: 11px; color: var(--kiss-muted); margin-top: 2px;' },
(cat.apps || 0) + ' apps · ' + (cat.flows || 0) + ' flows')
]);
});
},
renderProtocolsList: function(protocols) {
var total = protocols.reduce(function(sum, p) { return sum + (p.count || 0); }, 0);
return protocols.map(function(proto) {
var pct = total > 0 ? Math.round((proto.count / total) * 100) : 0;
var color = proto.name === 'TCP' ? 'var(--kiss-blue)' :
proto.name === 'UDP' ? 'var(--kiss-green)' : 'var(--kiss-yellow)';
return E('div', { 'class': 'kiss-stat', 'style': 'text-align: left; padding: 16px;' }, [
E('div', { 'style': 'display: flex; justify-content: space-between; margin-bottom: 8px;' }, [
E('span', { 'style': 'font-weight: 600;' }, proto.name),
E('span', { 'style': 'color: var(--kiss-muted);' }, formatNumber(proto.count))
]),
E('div', { 'style': 'height: 6px; background: var(--kiss-line); border-radius: 3px; overflow: hidden;' }, [
E('div', { 'style': 'height: 100%; width: ' + pct + '%; background: ' + color + '; border-radius: 3px;' })
]),
E('div', { 'style': 'font-size: 11px; color: var(--kiss-muted); margin-top: 4px; text-align: center;' }, pct + '%')
]);
});
},
togglePoll: function(ev) {
var btn = ev.currentTarget;
var state = document.getElementById('poll-state');
if (this.pollActive) {
this.pollActive = false;
poll.stop();
btn.textContent = '▶ Resume';
if (state) {
state.textContent = 'Paused';
state.style.color = 'var(--kiss-yellow)';
}
} else {
this.pollActive = true;
this.startPolling();
btn.textContent = '⏸ Pause';
if (state) {
state.textContent = 'Active';
state.style.color = 'var(--kiss-green)';
}
}
},
startPolling: function() {
var self = this;
poll.add(L.bind(function() {
if (!this.pollActive) return Promise.resolve();
return this.refresh();
}, this), this.pollInterval);
},
refresh: function() {
var self = this;
return Promise.all([
callGetFlows(),
callGetApplications(),
callGetCategories()
]).then(function(data) {
var flowsData = data[0] || {};
var applications = data[1].applications || data[1] || [];
var categories = data[2].categories || data[2] || [];
var flows = flowsData.flows || [];
var activeFlows = flowsData.active || 0;
var totalFlows = flowsData.total || flows.length;
// Update stats
var activeEl = document.querySelector('[data-stat="active-flows"]');
var totalEl = document.querySelector('[data-stat="total-flows"]');
var countEl = document.querySelector('[data-stat="flows-count"]');
if (activeEl) activeEl.textContent = formatNumber(activeFlows);
if (totalEl) totalEl.textContent = formatNumber(totalFlows);
if (countEl) countEl.textContent = flows.length + ' detected';
// Update flows table
var table = document.getElementById('flows-table');
if (table && flows.length > 0) {
while (table.rows.length > 1) table.deleteRow(1);
flows.slice(0, 50).forEach(function(flow) {
table.appendChild(self.renderFlowRow(flow));
});
}
// Update apps list
if (Array.isArray(applications) && applications.length > 0) {
var appsList = document.getElementById('apps-list');
if (appsList) {
dom.content(appsList, self.renderAppsList(applications));
}
}
// Update categories list
if (Array.isArray(categories) && categories.length > 0) {
var catsList = document.getElementById('categories-list');
if (catsList) {
dom.content(catsList, self.renderCategoriesList(categories));
}
}
}).catch(function(err) {
console.error('Refresh failed:', err);
});
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});