- Remove unused application field concatenation causing "TCPnull" display - Sort protocols by flow count instead of non-existent bytes field - Simplify protocol card to show protocol name and flow count only Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
269 lines
11 KiB
JavaScript
269 lines
11 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require dom';
|
|
'require poll';
|
|
'require rpc';
|
|
'require ui';
|
|
|
|
var callLanStatus = rpc.declare({
|
|
object: 'luci.dpi-dual',
|
|
method: 'get_lan_status',
|
|
expect: {}
|
|
});
|
|
|
|
var callLanClients = rpc.declare({
|
|
object: 'luci.dpi-dual',
|
|
method: 'get_lan_clients',
|
|
expect: {}
|
|
});
|
|
|
|
var callLanDestinations = rpc.declare({
|
|
object: 'luci.dpi-dual',
|
|
method: 'get_lan_destinations',
|
|
params: ['limit'],
|
|
expect: {}
|
|
});
|
|
|
|
var callLanProtocols = rpc.declare({
|
|
object: 'luci.dpi-dual',
|
|
method: 'get_lan_protocols',
|
|
expect: {}
|
|
});
|
|
|
|
function formatBytes(bytes) {
|
|
if (bytes === 0) return '0 B';
|
|
var k = 1024;
|
|
var sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
var i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
}
|
|
|
|
function formatRelativeTime(timestamp) {
|
|
var now = Math.floor(Date.now() / 1000);
|
|
var diff = now - timestamp;
|
|
if (diff < 60) return diff + 's ago';
|
|
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
|
if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
|
|
return Math.floor(diff / 86400) + 'd ago';
|
|
}
|
|
|
|
function createLED(active, label) {
|
|
var color = active ? '#00d4aa' : '#ff4d4d';
|
|
return E('div', { 'style': 'display:flex;align-items:center;gap:8px;' }, [
|
|
E('span', {
|
|
'style': 'width:12px;height:12px;border-radius:50%;background:' + color +
|
|
';box-shadow:0 0 8px ' + color + ';'
|
|
}),
|
|
E('span', { 'style': 'color:#e0e0e0;' }, label)
|
|
]);
|
|
}
|
|
|
|
function createMetricCard(label, value, color) {
|
|
return E('div', {
|
|
'style': 'background:#1a1a24;padding:1rem;border-radius:8px;text-align:center;min-width:100px;'
|
|
}, [
|
|
E('div', {
|
|
'style': 'font-size:1.5rem;font-weight:700;color:' + (color || '#00d4aa') + ';font-family:monospace;'
|
|
}, String(value)),
|
|
E('div', {
|
|
'style': 'font-size:0.75rem;color:#808090;text-transform:uppercase;margin-top:4px;'
|
|
}, label)
|
|
]);
|
|
}
|
|
|
|
return view.extend({
|
|
load: function() {
|
|
return Promise.all([
|
|
callLanStatus().catch(function() { return {}; }),
|
|
callLanClients().catch(function() { return { clients: [] }; }),
|
|
callLanDestinations(100).catch(function() { return { destinations: [] }; }),
|
|
callLanProtocols().catch(function() { return { protocols: [] }; })
|
|
]);
|
|
},
|
|
|
|
render: function(data) {
|
|
var status = data[0] || {};
|
|
var clients = data[1] || {};
|
|
var destinations = data[2] || {};
|
|
var protocols = data[3] || {};
|
|
|
|
var view = E('div', { 'class': 'cbi-map', 'id': 'lan-flows-view' });
|
|
|
|
// Header section
|
|
var header = E('div', {
|
|
'style': 'background:linear-gradient(135deg,#1a1a2e 0%,#16213e 100%);padding:1.5rem;border-radius:12px;margin-bottom:1.5rem;border-left:4px solid #00a0ff;'
|
|
}, [
|
|
E('div', { 'style': 'display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:1rem;' }, [
|
|
E('div', {}, [
|
|
E('h2', { 'style': 'margin:0;color:#fff;font-size:1.4rem;' }, 'LAN Flow Analysis'),
|
|
E('p', { 'style': 'margin:0.5rem 0 0;color:#808090;font-size:0.9rem;' },
|
|
'Real-time passive flow monitoring on ' + (status.interface || 'br-lan') + ' - No MITM, no caching')
|
|
]),
|
|
E('div', { 'style': 'display:flex;gap:1rem;' }, [
|
|
createLED(status.collector_running, 'Collector'),
|
|
createLED(status.enabled, 'Enabled')
|
|
])
|
|
])
|
|
]);
|
|
|
|
// Metrics row
|
|
var metrics = E('div', {
|
|
'style': 'display:flex;gap:1rem;margin-bottom:1.5rem;flex-wrap:wrap;'
|
|
}, [
|
|
createMetricCard('Active Clients', status.active_clients || 0, '#00d4aa'),
|
|
createMetricCard('Destinations', status.unique_destinations || 0, '#00a0ff'),
|
|
createMetricCard('Protocols', status.detected_protocols || 0, '#ffa500'),
|
|
createMetricCard('RX', formatBytes(status.rx_bytes || 0), '#00d4aa'),
|
|
createMetricCard('TX', formatBytes(status.tx_bytes || 0), '#ff6b6b')
|
|
]);
|
|
|
|
// Main content - three columns
|
|
var content = E('div', {
|
|
'style': 'display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:1.5rem;'
|
|
});
|
|
|
|
// Clients table
|
|
var clientsCard = E('div', {
|
|
'style': 'background:#12121a;border-radius:12px;padding:1rem;'
|
|
}, [
|
|
E('h3', { 'style': 'margin:0 0 1rem;color:#00d4aa;font-size:1rem;border-bottom:1px solid #2a2a3a;padding-bottom:0.5rem;' },
|
|
'Active Clients'),
|
|
E('div', { 'id': 'clients-list', 'style': 'max-height:400px;overflow-y:auto;' })
|
|
]);
|
|
|
|
var clientsList = clientsCard.querySelector('#clients-list');
|
|
var clientsData = (clients.clients || []).sort(function(a, b) {
|
|
return (b.bytes_in + b.bytes_out) - (a.bytes_in + a.bytes_out);
|
|
});
|
|
|
|
if (clientsData.length === 0) {
|
|
clientsList.appendChild(E('div', {
|
|
'style': 'color:#808090;text-align:center;padding:2rem;'
|
|
}, 'No active clients detected'));
|
|
} else {
|
|
clientsData.forEach(function(client) {
|
|
var totalBytes = (client.bytes_in || 0) + (client.bytes_out || 0);
|
|
clientsList.appendChild(E('div', {
|
|
'style': 'background:#1a1a24;padding:0.75rem;border-radius:6px;margin-bottom:0.5rem;'
|
|
}, [
|
|
E('div', { 'style': 'display:flex;justify-content:space-between;align-items:center;' }, [
|
|
E('span', { 'style': 'font-family:monospace;color:#fff;font-weight:600;' }, client.ip),
|
|
E('span', { 'style': 'color:#00d4aa;font-size:0.85rem;' }, formatBytes(totalBytes))
|
|
]),
|
|
E('div', { 'style': 'display:flex;gap:1rem;margin-top:0.5rem;font-size:0.75rem;color:#808090;' }, [
|
|
E('span', {}, 'Flows: ' + (client.flows || 0)),
|
|
E('span', {}, client.last_proto || ''),
|
|
E('span', {}, client.last_app || ''),
|
|
client.last_seen ? E('span', {}, formatRelativeTime(client.last_seen)) : null
|
|
].filter(Boolean))
|
|
]));
|
|
});
|
|
}
|
|
|
|
// Protocols table
|
|
var protocolsCard = E('div', {
|
|
'style': 'background:#12121a;border-radius:12px;padding:1rem;'
|
|
}, [
|
|
E('h3', { 'style': 'margin:0 0 1rem;color:#ffa500;font-size:1rem;border-bottom:1px solid #2a2a3a;padding-bottom:0.5rem;' },
|
|
'Detected Protocols'),
|
|
E('div', { 'id': 'protocols-list', 'style': 'max-height:400px;overflow-y:auto;' })
|
|
]);
|
|
|
|
var protocolsList = protocolsCard.querySelector('#protocols-list');
|
|
var protocolsData = (protocols.protocols || []).sort(function(a, b) {
|
|
return (b.flows || 0) - (a.flows || 0);
|
|
});
|
|
|
|
if (protocolsData.length === 0) {
|
|
protocolsList.appendChild(E('div', {
|
|
'style': 'color:#808090;text-align:center;padding:2rem;'
|
|
}, 'No protocols detected'));
|
|
} else {
|
|
protocolsData.forEach(function(proto) {
|
|
var protoName = proto.protocol || 'Unknown';
|
|
var flowCount = proto.flows || 0;
|
|
|
|
protocolsList.appendChild(E('div', {
|
|
'style': 'background:#1a1a24;padding:0.75rem;border-radius:6px;margin-bottom:0.5rem;display:flex;justify-content:space-between;align-items:center;'
|
|
}, [
|
|
E('span', { 'style': 'color:#fff;font-weight:500;' }, protoName),
|
|
E('div', { 'style': 'text-align:right;' }, [
|
|
E('div', { 'style': 'color:#ffa500;font-size:0.85rem;' }, flowCount + ' flows')
|
|
])
|
|
]));
|
|
});
|
|
}
|
|
|
|
// Destinations table
|
|
var destinationsCard = E('div', {
|
|
'style': 'background:#12121a;border-radius:12px;padding:1rem;'
|
|
}, [
|
|
E('h3', { 'style': 'margin:0 0 1rem;color:#00a0ff;font-size:1rem;border-bottom:1px solid #2a2a3a;padding-bottom:0.5rem;' },
|
|
'External Destinations'),
|
|
E('div', { 'id': 'destinations-list', 'style': 'max-height:400px;overflow-y:auto;' })
|
|
]);
|
|
|
|
var destinationsList = destinationsCard.querySelector('#destinations-list');
|
|
var destinationsData = (destinations.destinations || []).sort(function(a, b) {
|
|
return (b.hits || 0) - (a.hits || 0);
|
|
});
|
|
|
|
if (destinationsData.length === 0) {
|
|
destinationsList.appendChild(E('div', {
|
|
'style': 'color:#808090;text-align:center;padding:2rem;'
|
|
}, 'No external destinations'));
|
|
} else {
|
|
destinationsData.slice(0, 50).forEach(function(dest) {
|
|
destinationsList.appendChild(E('div', {
|
|
'style': 'background:#1a1a24;padding:0.75rem;border-radius:6px;margin-bottom:0.5rem;'
|
|
}, [
|
|
E('div', { 'style': 'display:flex;justify-content:space-between;align-items:center;' }, [
|
|
E('span', { 'style': 'font-family:monospace;color:#fff;font-size:0.85rem;' },
|
|
dest.ip + ':' + (dest.port || '?')),
|
|
E('span', { 'style': 'color:#00a0ff;font-size:0.85rem;' }, formatBytes(dest.bytes || 0))
|
|
]),
|
|
E('div', { 'style': 'display:flex;gap:1rem;margin-top:0.25rem;font-size:0.7rem;color:#808090;' }, [
|
|
E('span', {}, dest.proto || ''),
|
|
E('span', {}, (dest.hits || 0) + ' hits'),
|
|
dest.last_seen ? E('span', {}, formatRelativeTime(dest.last_seen)) : null
|
|
].filter(Boolean))
|
|
]));
|
|
});
|
|
}
|
|
|
|
content.appendChild(clientsCard);
|
|
content.appendChild(protocolsCard);
|
|
content.appendChild(destinationsCard);
|
|
|
|
view.appendChild(header);
|
|
view.appendChild(metrics);
|
|
view.appendChild(content);
|
|
|
|
// Setup polling for real-time updates
|
|
poll.add(L.bind(this.pollData, this), 5);
|
|
|
|
return view;
|
|
},
|
|
|
|
pollData: function() {
|
|
var self = this;
|
|
|
|
return Promise.all([
|
|
callLanStatus().catch(function() { return {}; }),
|
|
callLanClients().catch(function() { return { clients: [] }; }),
|
|
callLanDestinations(100).catch(function() { return { destinations: [] }; }),
|
|
callLanProtocols().catch(function() { return { protocols: [] }; })
|
|
]).then(function(data) {
|
|
var view = document.getElementById('lan-flows-view');
|
|
if (!view) return;
|
|
|
|
// Update would require DOM manipulation
|
|
// For now, the page auto-refreshes via poll
|
|
});
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|