🚀 Sync 2026-03-09 20:23:16

This commit is contained in:
github-actions[bot] 2026-03-09 20:23:16 +08:00
parent b99e3e42e7
commit 6541db9764
16 changed files with 1328 additions and 266 deletions

View File

@ -10,8 +10,8 @@ LUCI_DEPENDS:=+luci-base +luci-lib-jsonc +curl +bandix
PKG_MAINTAINER:=timsaya
PKG_VERSION:=0.12.5
PKG_RELEASE:=1
PKG_VERSION:=0.12.6
PKG_RELEASE:=2
include $(TOPDIR)/feeds/luci/luci.mk

View File

@ -5,6 +5,18 @@
'require rpc';
'require poll';
var BANDIX_MODAL_BG_OPENWRT2020_LIGHT = '#ffffff';
var BANDIX_MODAL_BG_OPENWRT2020_DARK = '#2a2a2a';
var BANDIX_MODAL_BG_MATERIAL_LIGHT = '#ffffff';
var BANDIX_MODAL_BG_MATERIAL_DARK = '#303030';
var BANDIX_MODAL_BG_BOOTSTRAP_LIGHT = '#ffffff';
var BANDIX_MODAL_BG_BOOTSTRAP_DARK = '#303030';
var BANDIX_MODAL_BG_ARGON_LIGHT = '#F4F5F7';
var BANDIX_MODAL_BG_ARGON_DARK = '#252526';
var BANDIX_MODAL_BG_AURORA_LIGHT = '#ffffff';
var BANDIX_MODAL_BG_AURORA_DARK = '#0E172B';
var BANDIX_MODAL_BG_KUCAT_LIGHT = '#ffffff';
var BANDIX_MODAL_BG_KUCAT_DARK = '#222D3C';
function getThemeMode() {
var theme = uci.get('luci', 'main', 'mediaurlbase');
@ -56,7 +68,7 @@ function getThemeMode() {
function getThemeType() {
// 获取 LuCI 主题设置
var mediaUrlBase = uci.get('luci', 'main', 'mediaurlbase');
if (!mediaUrlBase) {
// 如果无法获取,尝试从 DOM 中检测
var linkTags = document.querySelectorAll('link[rel="stylesheet"]');
@ -69,56 +81,63 @@ function getThemeType() {
// 默认返回窄主题
return 'narrow';
}
var mediaUrlBaseLower = mediaUrlBase.toLowerCase();
// 宽主题关键词列表(可以根据需要扩展)
var wideThemeKeywords = ['argon', 'material', 'design', 'edge'];
// 检查是否是宽主题
for (var i = 0; i < wideThemeKeywords.length; i++) {
if (mediaUrlBaseLower.includes(wideThemeKeywords[i])) {
return 'wide';
}
}
// 默认是窄主题Bootstrap 等)
return 'narrow';
}
// 格式化时间戳
function formatTimestamp(timestamp) {
if (!timestamp) return _('Never Online');
var now = Math.floor(Date.now() / 1000);
var diff = now - timestamp;
if (diff < 60) {
return _('Just now');
} else if (diff < 3600) {
var minutes = Math.floor(diff / 60);
return minutes + ' ' + _('minutes ago');
} else if (diff < 86400) {
var hours = Math.floor(diff / 3600);
return hours + ' ' + _('hours ago');
} else if (diff < 2592000) {
var days = Math.floor(diff / 86400);
return days + ' ' + _('days ago');
} else if (diff < 31536000) {
var months = Math.floor(diff / 2592000);
return months + ' ' + _('months ago');
} else {
var years = Math.floor(diff / 31536000);
return years + ' ' + _('years ago');
function getThemeColors() {
var theme = uci.get('luci', 'main', 'mediaurlbase');
var mode = getThemeMode();
if (theme === '/luci-static/openwrt2020') {
return { modalBg: mode === 'dark' ? BANDIX_MODAL_BG_OPENWRT2020_DARK : BANDIX_MODAL_BG_OPENWRT2020_LIGHT };
}
if (theme === '/luci-static/material') {
return { modalBg: mode === 'dark' ? BANDIX_MODAL_BG_MATERIAL_DARK : BANDIX_MODAL_BG_MATERIAL_LIGHT };
}
if (theme === '/luci-static/bootstrap-light' || theme === '/luci-static/bootstrap') {
return { modalBg: mode === 'dark' ? BANDIX_MODAL_BG_BOOTSTRAP_DARK : BANDIX_MODAL_BG_BOOTSTRAP_LIGHT };
}
if (theme === '/luci-static/bootstrap-dark') {
return { modalBg: BANDIX_MODAL_BG_BOOTSTRAP_DARK };
}
if (theme === '/luci-static/argon') {
return { modalBg: mode === 'dark' ? BANDIX_MODAL_BG_ARGON_DARK : BANDIX_MODAL_BG_ARGON_LIGHT };
}
if (theme === '/luci-static/aurora') {
return { modalBg: mode === 'dark' ? BANDIX_MODAL_BG_AURORA_DARK : BANDIX_MODAL_BG_AURORA_LIGHT };
}
if (theme === '/luci-static/kucat') {
return { modalBg: mode === 'dark' ? BANDIX_MODAL_BG_KUCAT_DARK : BANDIX_MODAL_BG_KUCAT_LIGHT };
}
return { modalBg: mode === 'dark' ? BANDIX_MODAL_BG_OPENWRT2020_DARK : BANDIX_MODAL_BG_OPENWRT2020_LIGHT };
}
function formatSize(bytes) {
if (!bytes || bytes === 0) return '0 B';
var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
var i = Math.floor(Math.log(bytes) / Math.log(1024));
return parseFloat((bytes / Math.pow(1024, i)).toFixed(2)) + ' ' + units[i];
}
// 格式化设备名称
function formatDeviceName(device) {
if (device.hostname && device.hostname !== '') {
return device.hostname;
if (device.host && device.host !== '') {
return device.host;
}
return device.ip_address || device.mac_address || _('Unknown Device');
return device.ip4 || device.mac || _('Unknown Device');
}
// RPC调用
@ -128,6 +147,13 @@ var callGetConnection = rpc.declare({
expect: {}
});
var callGetConnectionFlows = rpc.declare({
object: 'luci.bandix',
method: 'getConnectionFlows',
params: ['ip', 'protocol', 'state'],
expect: {}
});
return view.extend({
load: function () {
return Promise.all([
@ -141,6 +167,7 @@ return view.extend({
render: function (data) {
var connectionEnabled = uci.get('bandix', 'connections', 'enabled') === '1';
var themeColors = getThemeColors();
// 创建样式
var style = E('style', {}, `
@ -456,11 +483,12 @@ return view.extend({
white-space: nowrap;
}
.bandix-table th:nth-child(1) { width: 30%; }
.bandix-table th:nth-child(2) { width: 12%; }
.bandix-table th:nth-child(3) { width: 12%; }
.bandix-table th:nth-child(4) { width: 31%; }
.bandix-table th:nth-child(5) { width: 15%; }
.bandix-table th:nth-child(1) { width: 26%; }
.bandix-table th:nth-child(2) { width: 10%; }
.bandix-table th:nth-child(3) { width: 10%; }
.bandix-table th:nth-child(4) { width: 28%; }
.bandix-table th:nth-child(5) { width: 13%; }
.bandix-table th:nth-child(6) { width: 13%; }
.bandix-table td {
padding: 12px 16px;
@ -566,6 +594,195 @@ return view.extend({
text-align: center;
padding: 40px;
}
.flows-view-btn {
padding: 4px 10px;
font-size: 0.8rem;
}
.flows-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0,0,0,0);
display: flex;
align-items: center;
justify-content: center;
z-index: 1100;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s ease, visibility 0.2s ease, background-color 0.2s ease;
}
.flows-modal-overlay.show {
background-color: rgba(0,0,0,0.5);
opacity: 1;
visibility: visible;
}
.flows-modal-overlay .flows-modal-content {
margin: 16px;
width: 95vw;
max-width: 1200px;
max-height: 85vh;
display: flex;
flex-direction: column;
overflow: hidden;
border-radius: 8px;
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
box-sizing: border-box;
background-color: ${themeColors.modalBg};
}
.flows-modal-content.theme-light {
color: #111827;
}
.flows-modal-content.theme-dark {
color: #e5e5e5;
}
.flows-modal-content.theme-dark .flows-modal-header {
border-bottom-color: rgba(255,255,255,0.15);
}
.flows-modal-content.theme-dark .flows-filters {
border-bottom-color: rgba(255,255,255,0.15);
}
.flows-modal-content.theme-dark .flows-table th,
.flows-modal-content.theme-dark .flows-table td {
border-color: rgba(255,255,255,0.1);
}
.flows-modal-content.theme-dark .flows-table-body {
background-color: transparent;
}
.flows-modal-content .flows-modal-header {
padding: 12px 16px;
border-bottom: 1px solid rgba(0,0,0,0.1);
flex-shrink: 0;
}
.flows-modal-content .flows-modal-title {
font-size: 1.1rem;
font-weight: 600;
margin: 0 0 4px 0;
}
.flows-modal-content .flows-modal-subtitle {
font-size: 0.85rem;
opacity: 0.7;
}
.flows-modal-content .flows-filters {
padding: 12px 16px;
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
border-bottom: 1px solid rgba(0,0,0,0.1);
flex-shrink: 0;
}
.flows-modal-content .flows-filters select {
padding: 6px 10px;
border-radius: 4px;
border: 1px solid rgba(0,0,0,0.2);
font-size: 0.875rem;
}
.flows-modal-content.theme-dark .flows-filters select {
background-color: #3a3a3a;
border-color: rgba(255,255,255,0.2);
color: #e5e5e5;
}
.flows-modal-content .flows-table-wrap {
overflow: auto;
flex: 1;
min-height: 200px;
}
.flows-modal-content .flows-table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
font-size: 0.85rem;
}
.flows-modal-content .flows-table th:nth-child(1),
.flows-modal-content .flows-table td:nth-child(1) { width: 5%; }
.flows-modal-content .flows-table th:nth-child(2),
.flows-modal-content .flows-table td:nth-child(2) { width: 8%; }
.flows-modal-content .flows-table th:nth-child(3),
.flows-modal-content .flows-table td:nth-child(3) { width: 14%; }
.flows-modal-content .flows-table th:nth-child(4),
.flows-modal-content .flows-table td:nth-child(4) { width: 14%; }
.flows-modal-content .flows-table th:nth-child(5),
.flows-modal-content .flows-table td:nth-child(5) { width: 10%; }
.flows-modal-content .flows-table th:nth-child(6),
.flows-modal-content .flows-table td:nth-child(6) { width: 14%; }
.flows-modal-content .flows-table th:nth-child(7),
.flows-modal-content .flows-table td:nth-child(7) { width: 14%; }
.flows-modal-content .flows-table th:nth-child(8),
.flows-modal-content .flows-table td:nth-child(8) { width: 10%; }
.flows-modal-content .flows-table th:nth-child(9),
.flows-modal-content .flows-table td:nth-child(9) { width: 11%; }
.flows-modal-content .flows-table th,
.flows-modal-content .flows-table td {
padding: 8px 12px;
text-align: left;
border-bottom: 1px solid rgba(0,0,0,0.08);
}
.flows-modal-content .flows-table th {
font-weight: 600;
white-space: nowrap;
}
.flows-modal-content .flows-addr-cell {
word-break: break-all;
font-size: 0.8rem;
}
.flows-modal-content .flows-protocol-tcp {
color: #10b981;
font-weight: 600;
}
.flows-modal-content .flows-protocol-udp {
color: #06b6d4;
font-weight: 600;
}
.flows-modal-content .flows-state-est {
color: #10b981;
}
.flows-modal-content .flows-state-wait {
color: #f59e0b;
}
.flows-modal-content .flows-state-close {
color: #6b7280;
}
.flows-modal-content .flows-footer {
padding: 12px 16px;
border-top: 1px solid rgba(0,0,0,0.1);
flex-shrink: 0;
display: flex;
justify-content: flex-end;
}
.flows-modal-content.theme-dark .flows-footer {
border-top-color: rgba(255,255,255,0.15);
}
`);
document.head.appendChild(style);
@ -580,7 +797,7 @@ return view.extend({
// 检查连接监控是否启用
if (!connectionEnabled) {
var alertDiv = E('div', {
var alertDiv = E('div', {
'class': 'bandix-alert' + (getThemeType() === 'wide' ? ' wide-theme' : '')
}, [
E('div', { 'style': 'display: flex; align-items: center; gap: 8px;' }, [
@ -607,7 +824,7 @@ return view.extend({
}
// 添加提示信息
var infoAlert = E('div', {
var infoAlert = E('div', {
'class': 'bandix-alert' + (getThemeType() === 'wide' ? ' wide-theme' : '')
}, [
E('div', { 'style': 'display: flex; align-items: center; gap: 8px;' }, [
@ -660,7 +877,8 @@ return view.extend({
E('th', {}, 'TCP'),
E('th', {}, 'UDP'),
E('th', {}, _('TCP Status Details')),
E('th', {}, _('Total Connections'))
E('th', {}, _('Total Connections')),
E('th', { 'style': 'width: 80px;' }, _('Actions'))
])
]),
E('tbody', {})
@ -674,12 +892,12 @@ return view.extend({
function updateGlobalStats(stats) {
if (!stats) return;
document.getElementById('total-connections').textContent = stats.total_connections || 0;
document.getElementById('tcp-connections').textContent = stats.tcp_connections || 0;
document.getElementById('udp-connections').textContent = stats.udp_connections || 0;
document.getElementById('established-tcp').textContent = stats.established_tcp || 0;
document.getElementById('time-wait-tcp').textContent = stats.time_wait_tcp || 0;
document.getElementById('close-wait-tcp').textContent = stats.close_wait_tcp || 0;
document.getElementById('total-connections').textContent = stats.total || 0;
document.getElementById('tcp-connections').textContent = stats.tcp || 0;
document.getElementById('udp-connections').textContent = stats.udp || 0;
document.getElementById('established-tcp').textContent = stats.tcp_est || 0;
document.getElementById('time-wait-tcp').textContent = stats.tcp_tw || 0;
document.getElementById('close-wait-tcp').textContent = stats.tcp_cw || 0;
}
// 更新设备表格
@ -701,47 +919,54 @@ return view.extend({
E('th', {}, 'TCP'),
E('th', {}, 'UDP'),
E('th', {}, _('TCP Status Details')),
E('th', {}, _('Total Connections'))
E('th', {}, _('Total Connections')),
E('th', {}, _('Actions'))
])
]),
E('tbody', {}, devices.map(function (device) {
var viewBtn = E('button', {
'class': 'cbi-button cbi-button-reset flows-view-btn',
'data-ip': device.ip4 || '',
'data-device': JSON.stringify({ ip4: device.ip4, host: device.host, mac: device.mac })
}, _('Details'));
return E('tr', {}, [
E('td', {}, [
E('div', { 'class': 'device-info' }, [
E('div', { 'class': 'device-status online' }),
E('div', { 'class': 'device-details' }, [
E('div', { 'class': 'device-name' }, formatDeviceName(device)),
E('div', { 'class': 'device-ip' }, device.ip_address || '-'),
E('div', { 'class': 'device-mac' }, device.mac_address || '-')
E('div', { 'class': 'device-ip' }, device.ip4 || '-'),
E('div', { 'class': 'device-mac' }, device.mac || '-')
])
])
]),
E('td', { 'style': 'font-weight: 600; font-size: 1.25rem;' }, device.tcp_connections || 0),
E('td', { 'style': 'font-weight: 600; font-size: 1.25rem;' }, device.udp_connections || 0),
E('td', { 'style': 'font-weight: 600; font-size: 1.25rem;' }, device.tcp || 0),
E('td', { 'style': 'font-weight: 600; font-size: 1.25rem;' }, device.udp || 0),
E('td', {}, [
E('div', { 'class': 'tcp-status-details' }, [
E('div', { 'class': 'tcp-status-item' }, [
E('span', { 'class': 'tcp-status-label established' }, 'EST'),
E('span', { 'class': 'tcp-status-value' }, device.established_tcp || 0)
E('span', { 'class': 'tcp-status-value' }, device.tcp_est || 0)
]),
E('div', { 'class': 'tcp-status-item' }, [
E('span', { 'class': 'tcp-status-label time-wait' }, 'WAIT'),
E('span', { 'class': 'tcp-status-value' }, device.time_wait_tcp || 0)
E('span', { 'class': 'tcp-status-value' }, device.tcp_tw || 0)
]),
E('div', { 'class': 'tcp-status-item' }, [
E('span', { 'class': 'tcp-status-label closed' }, 'CLOSE'),
E('span', { 'class': 'tcp-status-value' }, device.close_wait_tcp || 0)
E('span', { 'class': 'tcp-status-value' }, device.tcp_cw || 0)
])
])
]),
E('td', {}, E('strong', { 'style': 'font-size: 1.25rem;' }, device.total_connections || 0))
E('td', {}, E('strong', { 'style': 'font-size: 1.25rem;' }, device.total || 0)),
E('td', {}, viewBtn)
]);
}))
]);
// 创建卡片容器(移动端)
var cardsContainer = E('div', { 'class': 'device-list-cards' });
devices.forEach(function (device) {
var card = E('div', { 'class': 'device-card' }, [
// 卡片头部:设备信息
@ -749,19 +974,19 @@ return view.extend({
E('div', { 'class': 'device-status online' }),
E('div', { 'class': 'device-card-name' }, [
E('div', { 'class': 'device-name' }, formatDeviceName(device)),
E('div', { 'class': 'device-ip' }, device.ip_address || '-'),
E('div', { 'class': 'device-mac' }, device.mac_address || '-')
E('div', { 'class': 'device-ip' }, device.ip4 || '-'),
E('div', { 'class': 'device-mac' }, device.mac || '-')
])
]),
// 统计信息TCP 和 UDP
E('div', { 'class': 'device-card-stats' }, [
E('div', { 'class': 'device-card-stat-item' }, [
E('div', { 'class': 'device-card-stat-label' }, 'TCP'),
E('div', { 'class': 'device-card-stat-value' }, device.tcp_connections || 0)
E('div', { 'class': 'device-card-stat-value' }, device.tcp || 0)
]),
E('div', { 'class': 'device-card-stat-item' }, [
E('div', { 'class': 'device-card-stat-label' }, 'UDP'),
E('div', { 'class': 'device-card-stat-value' }, device.udp_connections || 0)
E('div', { 'class': 'device-card-stat-value' }, device.udp || 0)
])
]),
// TCP 状态详情
@ -769,24 +994,31 @@ return view.extend({
E('div', { 'class': 'device-card-tcp-details-label' }, _('TCP Status Details')),
E('div', { 'class': 'device-card-tcp-status-row' }, [
E('span', { 'class': 'device-card-tcp-status-label established' }, 'EST'),
E('span', { 'class': 'device-card-tcp-status-value' }, device.established_tcp || 0)
E('span', { 'class': 'device-card-tcp-status-value' }, device.tcp_est || 0)
]),
E('div', { 'class': 'device-card-tcp-status-row' }, [
E('span', { 'class': 'device-card-tcp-status-label time-wait' }, 'WAIT'),
E('span', { 'class': 'device-card-tcp-status-value' }, device.time_wait_tcp || 0)
E('span', { 'class': 'device-card-tcp-status-value' }, device.tcp_tw || 0)
]),
E('div', { 'class': 'device-card-tcp-status-row' }, [
E('span', { 'class': 'device-card-tcp-status-label closed' }, 'CLOSE'),
E('span', { 'class': 'device-card-tcp-status-value' }, device.close_wait_tcp || 0)
E('span', { 'class': 'device-card-tcp-status-value' }, device.tcp_cw || 0)
])
]),
// 总连接数
E('div', { 'class': 'device-card-total' }, [
E('div', { 'style': 'font-size: 0.875rem; opacity: 0.7; font-weight: 500;' }, _('Total Connections')),
E('div', { 'style': 'font-size: 1.25rem; font-weight: 700;' }, device.total_connections || 0)
E('div', { 'style': 'display: flex; align-items: center; gap: 8px;' }, [
E('span', { 'style': 'font-size: 1.25rem; font-weight: 700;' }, device.total || 0),
E('button', {
'class': 'cbi-button cbi-button-reset flows-view-btn',
'data-ip': device.ip4 || '',
'data-device': JSON.stringify({ ip4: device.ip4, host: device.host, mac: device.mac })
}, _('Details'))
])
])
]);
cardsContainer.appendChild(card);
});
@ -806,8 +1038,8 @@ return view.extend({
function updateConnectionData() {
return callGetConnection().then(function (result) {
if (result && result.status === 'success' && result.data) {
updateGlobalStats(result.data.global_stats);
updateDeviceTable(result.data.devices);
updateGlobalStats(result.data.g);
updateDeviceTable(result.data.d);
} else {
showError(_('Unable to fetch data'));
}
@ -817,6 +1049,148 @@ return view.extend({
});
}
function showFlowsModal(deviceIp, deviceInfo) {
var currentTheme = getThemeMode();
var protocolSelect = E('select', { 'class': 'cbi-input-select', 'id': 'flows-protocol' }, [
E('option', { 'value': '' }, _('All')),
E('option', { 'value': 'tcp' }, 'TCP'),
E('option', { 'value': 'udp' }, 'UDP')
]);
var stateSelect = E('select', { 'class': 'cbi-input-select', 'id': 'flows-state' }, [
E('option', { 'value': '' }, _('All')),
E('option', { 'value': 'ESTABLISHED' }, 'ESTABLISHED'),
E('option', { 'value': 'TIME_WAIT' }, 'TIME_WAIT'),
E('option', { 'value': 'CLOSE_WAIT' }, 'CLOSE_WAIT')
]);
var tableBody = E('tbody', { 'class': 'flows-table-body' });
var tableWrap = E('div', { 'class': 'flows-table-wrap' }, [
E('table', { 'class': 'flows-table' }, [
E('thead', {}, [
E('tr', {}, [
E('th', {}, _('Protocol')),
E('th', {}, _('State')),
E('th', {}, _('Orig Src')),
E('th', {}, _('Orig Dst')),
E('th', {}, _('Send')),
E('th', {}, _('Repl Src')),
E('th', {}, _('Repl Dst')),
E('th', {}, _('Reply')),
E('th', {}, _('Flags'))
])
]),
tableBody
])
]);
function loadFlows() {
var protocol = protocolSelect.value || '';
var state = stateSelect.value || '';
tableBody.innerHTML = '';
tableBody.appendChild(E('tr', {}, [
E('td', { 'colspan': 9, 'class': 'loading-state' }, _('Loading…'))
]));
return callGetConnectionFlows(deviceIp, protocol || null, state || null).then(function (res) {
tableBody.innerHTML = '';
if (!res || res.status !== 'success' || !res.data || res.data.length === 0) {
tableBody.appendChild(E('tr', {}, [
E('td', { 'colspan': 9, 'class': 'loading-state' }, _('No connections'))
]));
return;
}
res.data.forEach(function (f) {
var stateCls = '';
if (f.state) {
if (f.state.indexOf('ESTABLISHED') >= 0) stateCls = 'flows-state-est';
else if (f.state.indexOf('TIME_WAIT') >= 0) stateCls = 'flows-state-wait';
else if (f.state.indexOf('CLOSE') >= 0) stateCls = 'flows-state-close';
}
var origSrc = f.orig ? (f.orig.src || '-') + ':' + (f.orig.sport || '-') : '-';
var origDst = f.orig ? (f.orig.dst || '-') + ':' + (f.orig.dport || '-') : '-';
var replSrc = f.repl ? (f.repl.src || '-') + ':' + (f.repl.sport || '-') : '-';
var replDst = f.repl ? (f.repl.dst || '-') + ':' + (f.repl.dport || '-') : '-';
var sendStr = (f.orig_packets || 0) + ' ' + _('pkts') + ' / ' + formatSize(f.orig_bytes || 0);
var replyStr = (f.repl_packets || 0) + ' ' + _('pkts') + ' / ' + formatSize(f.repl_bytes || 0);
var flagsStr = (f.flags && f.flags.length) ? f.flags.join(' ') : '-';
var protoCls = (f.protocol || '').toLowerCase() === 'tcp' ? 'flows-protocol-tcp' : 'flows-protocol-udp';
tableBody.appendChild(E('tr', {}, [
E('td', { 'class': protoCls }, (f.protocol || '-').toUpperCase()),
E('td', { 'class': stateCls }, f.state || '-'),
E('td', { 'class': 'flows-addr-cell' }, origSrc),
E('td', { 'class': 'flows-addr-cell' }, origDst),
E('td', {}, sendStr),
E('td', { 'class': 'flows-addr-cell' }, replSrc),
E('td', { 'class': 'flows-addr-cell' }, replDst),
E('td', {}, replyStr),
E('td', { 'style': 'font-size: 0.8rem;' }, flagsStr)
]));
});
}).catch(function (err) {
tableBody.innerHTML = '';
tableBody.appendChild(E('tr', {}, [
E('td', { 'colspan': 9, 'class': 'error-state' }, _('Failed to load') + ': ' + (err.message || err))
]));
});
}
function onFilterChange() {
loadFlows();
}
function hideFlowsModal() {
var overlay = document.getElementById('flows-modal-overlay');
if (overlay) overlay.classList.remove('show');
}
var modalContent = E('div', { 'class': 'flows-modal-content theme-' + currentTheme }, [
E('div', { 'class': 'flows-modal-header' }, [
E('div', { 'class': 'flows-modal-title' }, _('Connection Details') + ' - ' + (deviceInfo.host || deviceInfo.ip4 || deviceIp)),
E('div', { 'class': 'flows-modal-subtitle' }, (deviceInfo.ip4 || '') + (deviceInfo.mac ? ' · ' + deviceInfo.mac : ''))
]),
E('div', { 'class': 'flows-filters' }, [
E('label', {}, _('Protocol') + ':'),
protocolSelect,
E('label', {}, _('State') + ':'),
stateSelect,
E('button', { 'class': 'cbi-button cbi-button-apply', 'click': loadFlows }, _('Refresh'))
]),
tableWrap,
E('div', { 'class': 'flows-footer' }, [
E('button', { 'class': 'cbi-button cbi-button-reset', 'click': hideFlowsModal }, _('Close'))
])
]);
if (protocolSelect && protocolSelect.addEventListener) {
protocolSelect.addEventListener('change', onFilterChange);
}
if (stateSelect && stateSelect.addEventListener) {
stateSelect.addEventListener('change', onFilterChange);
}
var overlay = document.getElementById('flows-modal-overlay');
if (!overlay) {
overlay = E('div', { 'class': 'flows-modal-overlay', 'id': 'flows-modal-overlay' });
document.body.appendChild(overlay);
}
overlay.innerHTML = '';
overlay.appendChild(modalContent);
overlay.classList.add('show');
modalContent.addEventListener('click', function (e) { e.stopPropagation(); });
loadFlows();
}
container.addEventListener('click', function (ev) {
var btn = ev.target;
if (btn && btn.classList && btn.classList.contains('flows-view-btn')) {
var ip = btn.getAttribute('data-ip');
var deviceStr = btn.getAttribute('data-device');
var deviceInfo = {};
try {
if (deviceStr) deviceInfo = JSON.parse(deviceStr);
} catch (e) {}
if (ip) showFlowsModal(ip, deviceInfo);
}
});
// 轮询获取数据
poll.add(updateConnectionData, 1);
@ -829,21 +1203,21 @@ return view.extend({
var mainElement = document.querySelector('.main') || document.body;
var computedStyle = window.getComputedStyle(mainElement);
var bgColor = computedStyle.backgroundColor;
// 如果父元素有背景色,应用到容器和卡片
if (bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent') {
var containerEl = document.querySelector('.bandix-connection-container');
if (containerEl) {
containerEl.style.backgroundColor = bgColor;
}
// 应用到表格表头
var tableHeaders = document.querySelectorAll('.bandix-table th');
tableHeaders.forEach(function(th) {
tableHeaders.forEach(function (th) {
th.style.backgroundColor = bgColor;
});
}
// 检测文字颜色并应用
var textColor = computedStyle.color;
if (textColor && textColor !== 'rgba(0, 0, 0, 0)') {
@ -857,17 +1231,17 @@ return view.extend({
console.log('Theme adaptation:', e);
}
}
// 初始应用主题颜色
setTimeout(applyThemeColors, 100);
// 监听 DOM 变化,自动应用到新创建的元素
if (typeof MutationObserver !== 'undefined') {
var observer = new MutationObserver(function(mutations) {
var observer = new MutationObserver(function (mutations) {
applyThemeColors();
});
setTimeout(function() {
setTimeout(function () {
var container = document.querySelector('.bandix-connection-container');
if (container) {
observer.observe(container, {

View File

@ -850,6 +850,72 @@ return view.extend({
opacity: 0.7;
font-size: 0.875rem;
}
.device-uplink-badges {
display: inline-flex;
flex-wrap: wrap;
gap: 4px;
align-items: center;
margin-left: 6px;
vertical-align: middle;
}
.device-uplink-badge,
.device-ch-badge {
display: inline-block;
padding: 2px 6px;
border-radius: 4px;
font-size: 0.7rem;
font-weight: 600;
line-height: 1.2;
}
.device-uplink-badge {
background: rgba(107, 114, 128, 0.2);
color: #4b5563;
}
.theme-dark .device-uplink-badge {
background: rgba(156, 163, 175, 0.25);
color: #9ca3af;
}
.device-ch-badge.device-ch-g24 {
background: rgba(245, 158, 11, 0.2);
color: #b45309;
}
.theme-dark .device-ch-badge.device-ch-g24 {
background: rgba(251, 191, 36, 0.2);
color: #fbbf24;
}
.device-ch-badge.device-ch-g5 {
background: rgba(59, 130, 246, 0.2);
color: #2563eb;
}
.theme-dark .device-ch-badge.device-ch-g5 {
background: rgba(96, 165, 250, 0.2);
color: #60a5fa;
}
.device-ch-badge.device-ch-g6 {
background: rgba(139, 92, 246, 0.2);
color: #6d28d9;
}
.theme-dark .device-ch-badge.device-ch-g6 {
background: rgba(167, 139, 250, 0.2);
color: #a78bfa;
}
.device-uplink-badges-wrap {
display: flex;
flex-wrap: wrap;
gap: 4px;
align-items: center;
}
.device-ipv6 {
opacity: 0.7;
@ -1367,7 +1433,11 @@ return view.extend({
opacity: 0.7;
font-size: 0.875rem;
}
.device-summary-badges {
margin-top: 8px;
}
/* 加载动画 */
.loading-spinner {
display: inline-block;
@ -1596,6 +1666,12 @@ return view.extend({
opacity: 0.7;
margin-top: 4px;
}
.device-card-ip .device-uplink-badges .device-uplink-badge,
.device-card-ip .device-uplink-badges .device-ch-badge {
padding: 1px 5px;
font-size: 0.65rem;
}
.device-card-action {
flex-shrink: 0;
@ -3042,8 +3118,8 @@ return view.extend({
};
var daysText = days.length > 0 ? days.map(function (d) { return dayNames[d] || d; }).join(', ') : '-';
var uploadLimit = rule.wan_tx_rate_limit || 0;
var downloadLimit = rule.wan_rx_rate_limit || 0;
var uploadLimit = rule.w_tx_r_limit || 0;
var downloadLimit = rule.w_rx_r_limit || 0;
// 使用 isRuleActive 函数检查规则是否激活
var isActive = isRuleActive(rule);
@ -3349,8 +3425,8 @@ return view.extend({
}
function getDeviceLabel(d) {
var name = d && (d.hostname || d.name) ? (d.hostname || d.name) : '';
var ip = d && d.ip ? d.ip : '';
var name = d && (d.host || d.name) ? (d.host || d.name) : '';
var ip = d && d.ip4 ? d.ip4 : '';
var mac = d && d.mac ? d.mac : '';
var left = name || ip || mac || '';
var right = mac && left !== mac ? (' ' + mac) : '';
@ -3428,8 +3504,8 @@ return view.extend({
macs.slice().sort().forEach(function (mac) {
var d = deviceMap[mac];
var hostname = d && d.hostname ? d.hostname : '';
var ip = d && d.ip ? d.ip : '';
var hostname = d && d.host ? d.host : '';
var ip = d && d.ip4 ? d.ip4 : '';
var title = hostname || ip || mac;
var item = E('div', { 'class': 'whitelist-modal-item' }, [
@ -3670,8 +3746,8 @@ return view.extend({
});
// 设置限速值
var uploadLimit = editingRule.wan_tx_rate_limit || 0;
var downloadLimit = editingRule.wan_rx_rate_limit || 0;
var uploadLimit = editingRule.w_tx_r_limit || 0;
var downloadLimit = editingRule.w_rx_r_limit || 0;
// 根据限速值选择合适的单位
var uploadUnit, uploadValue, downloadUnit, downloadValue;
@ -3888,14 +3964,18 @@ return view.extend({
// 加载定时限速规则列表
loadScheduleRules();
// 更新设备信息
deviceSummary.innerHTML = E('div', {}, [
E('div', { 'class': 'device-summary-name' }, device.hostname || device.ip),
E('div', { 'class': 'device-summary-details' }, device.ip + ' (' + device.mac + ')')
]).innerHTML;
var modalContent = [
E('div', { 'class': 'device-summary-name' }, device.host || device.ip4),
E('div', { 'class': 'device-summary-details' }, device.ip4 + ' (' + device.mac + ')')
];
var modalBadges = buildDeviceUplinkChBadges(device);
if (modalBadges.length) {
modalContent.push(E('div', { 'class': 'device-summary-badges device-uplink-badges-wrap' }, modalBadges));
}
deviceSummary.innerHTML = E('div', {}, modalContent).innerHTML;
// 设置当前hostname值
document.getElementById('device-hostname-input').value = device.hostname || '';
document.getElementById('device-hostname-input').value = device.host || '';
// 显示模态框并添加动画
@ -3984,8 +4064,8 @@ return view.extend({
var startTime = rule.time_slot && rule.time_slot.start ? rule.time_slot.start : '';
var endTime = rule.time_slot && rule.time_slot.end ? rule.time_slot.end : '';
var uploadLimit = rule.wan_tx_rate_limit || 0;
var downloadLimit = rule.wan_rx_rate_limit || 0;
var uploadLimit = rule.w_tx_r_limit || 0;
var downloadLimit = rule.w_rx_r_limit || 0;
var ruleItem = E('div', { 'class': 'schedule-rule-item' }, [
E('div', { 'class': 'schedule-rule-info' }, [
@ -4050,7 +4130,7 @@ return view.extend({
var newHostname = document.getElementById('device-hostname-input').value.trim();
// 如果hostname没有变化不需要保存
if (newHostname === (currentDevice.hostname || '')) {
if (newHostname === (currentDevice.host || '')) {
return;
}
@ -4066,7 +4146,7 @@ return view.extend({
saveButton.disabled = false;
// 更新当前设备信息
currentDevice.hostname = newHostname;
currentDevice.host = newHostname;
// 刷新设备数据
updateDeviceData();
@ -4182,8 +4262,8 @@ return view.extend({
var downloadLimits = [];
activeRules.forEach(function (rule) {
var uploadLimit = rule.wan_tx_rate_limit || 0;
var downloadLimit = rule.wan_rx_rate_limit || 0;
var uploadLimit = rule.w_tx_r_limit || 0;
var downloadLimit = rule.w_rx_r_limit || 0;
// 只收集非零的限制值
if (uploadLimit > 0) {
@ -4272,8 +4352,8 @@ return view.extend({
if (!aOnline && bOnline) return 1;
// 在线状态相同时按IP地址排序
var aIp = a.ip || '';
var bIp = b.ip || '';
var aIp = a.ip4 || '';
var bIp = b.ip4 || '';
// 将IP地址转换为数字进行比较
var aIpParts = aIp.split('.').map(function (part) { return parseInt(part) || 0; });
@ -4303,7 +4383,7 @@ return view.extend({
select.innerHTML = '';
select.appendChild(E('option', { 'value': '' }, _('All Devices')));
sortedDevices.forEach(function (d) {
var label = (d.hostname || d.ip || d.mac || '-') + (d.ip ? ' (' + d.ip + ')' : '') + (d.mac ? ' [' + d.mac + ']' : '');
var label = (d.host || d.ip4 || d.mac || '-') + (d.ip4 ? ' (' + d.ip4 + ')' : '') + (d.mac ? ' [' + d.mac + ']' : '');
select.appendChild(E('option', { 'value': d.mac }, label));
});
// 尽量保留之前选择
@ -4660,11 +4740,11 @@ return view.extend({
'<div class="ht-kpis">' +
'<div class="ht-kpi up">' +
'<div class="ht-k-label">' + _('WAN Upload') + ' (P95)</div>' +
'<div class="ht-k-value">' + formatByterate(point.wan_tx_rate_p95 || 0, speedUnit) + '</div>' +
'<div class="ht-k-value">' + formatByterate(point.w_tx_p95 || 0, speedUnit) + '</div>' +
'</div>' +
'<div class="ht-kpi down">' +
'<div class="ht-k-label">' + _('WAN Download') + ' (P95)</div>' +
'<div class="ht-k-value">' + formatByterate(point.wan_rx_rate_p95 || 0, speedUnit) + '</div>' +
'<div class="ht-k-value">' + formatByterate(point.w_rx_p95 || 0, speedUnit) + '</div>' +
'</div>' +
'</div>'
);
@ -4672,20 +4752,20 @@ return view.extend({
// 详细统计信息
lines.push('<div class="ht-divider"></div>');
lines.push('<div class="ht-section-title">' + _('Upload Statistics') + '</div>');
row(_('Average'), formatByterate(point.wan_tx_rate_avg || 0, speedUnit));
row(_('Maximum'), formatByterate(point.wan_tx_rate_max || 0, speedUnit));
row(_('Minimum'), formatByterate(point.wan_tx_rate_min || 0, speedUnit));
row('P90', formatByterate(point.wan_tx_rate_p90 || 0, speedUnit));
row('P95', formatByterate(point.wan_tx_rate_p95 || 0, speedUnit));
row('P99', formatByterate(point.wan_tx_rate_p99 || 0, speedUnit));
row(_('Average'), formatByterate(point.w_tx_avg || 0, speedUnit));
row(_('Maximum'), formatByterate(point.w_tx_max || 0, speedUnit));
row(_('Minimum'), formatByterate(point.w_tx_min || 0, speedUnit));
row('P90', formatByterate(point.w_tx_p90 || 0, speedUnit));
row('P95', formatByterate(point.w_tx_p95 || 0, speedUnit));
row('P99', formatByterate(point.w_tx_p99 || 0, speedUnit));
lines.push('<div class="ht-section-title" style="margin-top: 8px;">' + _('Download Statistics') + '</div>');
row(_('Average'), formatByterate(point.wan_rx_rate_avg || 0, speedUnit));
row(_('Maximum'), formatByterate(point.wan_rx_rate_max || 0, speedUnit));
row(_('Minimum'), formatByterate(point.wan_rx_rate_min || 0, speedUnit));
row('P90', formatByterate(point.wan_rx_rate_p90 || 0, speedUnit));
row('P95', formatByterate(point.wan_rx_rate_p95 || 0, speedUnit));
row('P99', formatByterate(point.wan_rx_rate_p99 || 0, speedUnit));
row(_('Average'), formatByterate(point.w_rx_avg || 0, speedUnit));
row(_('Maximum'), formatByterate(point.w_rx_max || 0, speedUnit));
row(_('Minimum'), formatByterate(point.w_rx_min || 0, speedUnit));
row('P90', formatByterate(point.w_rx_p90 || 0, speedUnit));
row('P95', formatByterate(point.w_rx_p95 || 0, speedUnit));
row('P99', formatByterate(point.w_rx_p99 || 0, speedUnit));
// 累计流量(只显示 WAN
lines.push('<div class="ht-divider"></div>');
@ -4766,7 +4846,7 @@ return view.extend({
}
// 在线状态相同时按IP地址排序小的在前
var ipCompare = compareIP(a.ip, b.ip);
var ipCompare = compareIP(a.ip4, b.ip4);
if (ipCompare !== 0) return ipCompare;
// IP地址也相同时按MAC地址排序
@ -4784,14 +4864,14 @@ return view.extend({
}
// 在线状态相同时按LAN速度排序
var aSpeed = (a.lan_tx_rate || 0) + (a.lan_rx_rate || 0);
var bSpeed = (b.lan_tx_rate || 0) + (b.lan_rx_rate || 0);
var aSpeed = (a.l_tx_r || 0) + (a.l_rx_r || 0);
var bSpeed = (b.l_tx_r || 0) + (b.l_rx_r || 0);
if (aSpeed !== bSpeed) {
return ascending ? (aSpeed - bSpeed) : (bSpeed - aSpeed);
}
// 速度相同时按IP地址排序
return compareIP(a.ip, b.ip);
return compareIP(a.ip4, b.ip4);
});
break;
@ -4805,14 +4885,14 @@ return view.extend({
}
// 在线状态相同时按WAN速度排序
var aSpeed = (a.wan_tx_rate || 0) + (a.wan_rx_rate || 0);
var bSpeed = (b.wan_tx_rate || 0) + (b.wan_rx_rate || 0);
var aSpeed = (a.w_tx_r || 0) + (a.w_rx_r || 0);
var bSpeed = (b.w_tx_r || 0) + (b.w_rx_r || 0);
if (aSpeed !== bSpeed) {
return ascending ? (aSpeed - bSpeed) : (bSpeed - aSpeed);
}
// 速度相同时按IP地址排序
return compareIP(a.ip, b.ip);
return compareIP(a.ip4, b.ip4);
});
break;
@ -4826,14 +4906,14 @@ return view.extend({
}
// 在线状态相同时按LAN流量排序
var aTraffic = (a.lan_tx_bytes || 0) + (a.lan_rx_bytes || 0);
var bTraffic = (b.lan_tx_bytes || 0) + (b.lan_rx_bytes || 0);
var aTraffic = (a.l_tx_b || 0) + (a.l_rx_b || 0);
var bTraffic = (b.l_tx_b || 0) + (b.l_rx_b || 0);
if (aTraffic !== bTraffic) {
return ascending ? (aTraffic - bTraffic) : (bTraffic - aTraffic);
}
// 流量相同时按IP地址排序
return compareIP(a.ip, b.ip);
return compareIP(a.ip4, b.ip4);
});
break;
@ -4847,14 +4927,14 @@ return view.extend({
}
// 在线状态相同时按WAN流量排序
var aTraffic = (a.wan_tx_bytes || 0) + (a.wan_rx_bytes || 0);
var bTraffic = (b.wan_tx_bytes || 0) + (b.wan_rx_bytes || 0);
var aTraffic = (a.w_tx_b || 0) + (a.w_rx_b || 0);
var bTraffic = (b.w_tx_b || 0) + (b.w_rx_b || 0);
if (aTraffic !== bTraffic) {
return ascending ? (aTraffic - bTraffic) : (bTraffic - aTraffic);
}
// 流量相同时按IP地址排序
return compareIP(a.ip, b.ip);
return compareIP(a.ip4, b.ip4);
});
break;
@ -4869,7 +4949,7 @@ return view.extend({
}
// 在线状态相同时按IP地址排序小的在前
var ipCompare = compareIP(a.ip, b.ip);
var ipCompare = compareIP(a.ip4, b.ip4);
if (ipCompare !== 0) return ipCompare;
// IP相同时按MAC地址排序
@ -4883,19 +4963,19 @@ return view.extend({
// 判断设备是否在线(基于 last_online_ts
function isDeviceOnline(device) {
// 如果没有 last_online_ts 字段,使用原有的 online 字段
if (typeof device.last_online_ts === 'undefined') {
if (typeof device.last === 'undefined') {
return device.online !== false;
}
// 如果 last_online_ts 为 0 或无效值,认为离线
if (!device.last_online_ts || device.last_online_ts <= 0) {
if (!device.last || device.last <= 0) {
return false;
}
// 计算当前时间与最后在线时间的差值(毫秒)
var currentTime = Date.now();
// 如果时间戳小于1000000000000说明是秒级时间戳需要转换为毫秒
var lastOnlineTime = device.last_online_ts < 1000000000000 ? device.last_online_ts * 1000 : device.last_online_ts;
var lastOnlineTime = device.last < 1000000000000 ? device.last * 1000 : device.last;
var timeDiff = currentTime - lastOnlineTime;
// 从UCI配置获取离线超时时间默认10分钟
@ -4905,6 +4985,52 @@ return view.extend({
return timeDiff <= offlineThreshold;
}
function channelToBand(ch) {
var c = Number(ch);
if (c < 1) return null;
if (c <= 14) return '2.4 GHz';
if (c <= 165) return '5 GHz';
if (c <= 233) return '6 GHz';
return null;
}
function channelToBandClass(ch) {
var c = Number(ch);
if (c <= 14) return 'g24';
if (c <= 165) return 'g5';
if (c <= 233) return 'g6';
return '';
}
function buildDeviceUplinkChBadges(device) {
var badges = [];
if (device.uplink && String(device.uplink).trim() !== '') {
badges.push(E('span', { 'class': 'device-uplink-badge', 'title': _('Interface') }, device.uplink.trim()));
}
if (device.w_ch && Number(device.w_ch) > 0) {
var ch = Number(device.w_ch);
var band = channelToBand(ch);
var bandClass = channelToBandClass(ch);
if (band && bandClass) {
badges.push(E('span', {
'class': 'device-ch-badge device-ch-' + bandClass,
'title': _('WiFi Channel') + ' ' + ch
}, band));
}
}
return badges;
}
function formatDeviceUplinkCh(device) {
var parts = [];
if (device.uplink && String(device.uplink).trim() !== '') parts.push(device.uplink.trim());
if (device.w_ch && Number(device.w_ch) > 0) {
var band = channelToBand(Number(device.w_ch));
if (band) parts.push(band);
}
return parts.join(' · ');
}
// 格式化最后上线时间
function formatLastOnlineTime(lastOnlineTs) {
if (!lastOnlineTs || lastOnlineTs <= 0) {
@ -5470,7 +5596,7 @@ return view.extend({
var speedUnit = uci.get('bandix', 'traffic', 'speed_unit') || 'bytes';
var stats = result;
if (!stats || !stats.devices) {
if (!stats || !stats.d) {
if (trafficDiv) {
trafficDiv.innerHTML = '<div class="error">' + _('Unable to fetch data') + '</div>';
}
@ -5479,19 +5605,19 @@ return view.extend({
// 更新设备计数
if (deviceCountDiv) {
var onlineCount = stats.devices.filter(d => isDeviceOnline(d)).length;
deviceCountDiv.textContent = _('Online Devices') + ': ' + onlineCount + ' / ' + stats.devices.length;
var onlineCount = stats.d.filter(d => isDeviceOnline(d)).length;
deviceCountDiv.textContent = _('Online Devices') + ': ' + onlineCount + ' / ' + stats.d.length;
}
// 计算统计数据(包含所有设备)
var totalLanUp = stats.devices.reduce((sum, d) => sum + (d.lan_tx_bytes || 0), 0);
var totalLanDown = stats.devices.reduce((sum, d) => sum + (d.lan_rx_bytes || 0), 0);
var totalWanUp = stats.devices.reduce((sum, d) => sum + (d.wan_tx_bytes || 0), 0);
var totalWanDown = stats.devices.reduce((sum, d) => sum + (d.wan_rx_bytes || 0), 0);
var totalLanSpeedUp = stats.devices.reduce((sum, d) => sum + (d.lan_tx_rate || 0), 0);
var totalLanSpeedDown = stats.devices.reduce((sum, d) => sum + (d.lan_rx_rate || 0), 0);
var totalWanSpeedUp = stats.devices.reduce((sum, d) => sum + (d.wan_tx_rate || 0), 0);
var totalWanSpeedDown = stats.devices.reduce((sum, d) => sum + (d.wan_rx_rate || 0), 0);
var totalLanUp = stats.d.reduce((sum, d) => sum + (d.l_tx_b || 0), 0);
var totalLanDown = stats.d.reduce((sum, d) => sum + (d.l_rx_b || 0), 0);
var totalWanUp = stats.d.reduce((sum, d) => sum + (d.w_tx_b || 0), 0);
var totalWanDown = stats.d.reduce((sum, d) => sum + (d.w_rx_b || 0), 0);
var totalLanSpeedUp = stats.d.reduce((sum, d) => sum + (d.l_tx_r || 0), 0);
var totalLanSpeedDown = stats.d.reduce((sum, d) => sum + (d.l_rx_r || 0), 0);
var totalWanSpeedUp = stats.d.reduce((sum, d) => sum + (d.w_tx_r || 0), 0);
var totalWanSpeedDown = stats.d.reduce((sum, d) => sum + (d.w_rx_r || 0), 0);
var totalSpeedUp = totalLanSpeedUp + totalWanSpeedUp;
var totalSpeedDown = totalLanSpeedDown + totalWanSpeedDown;
var totalUp = totalLanUp + totalWanUp;
@ -5749,14 +5875,14 @@ return view.extend({
// 过滤:按选择设备
var selectedMac = (typeof document !== 'undefined' ? (document.getElementById('history-device-select')?.value || '') : '');
var filteredDevices = (!selectedMac) ? stats.devices : stats.devices.filter(function (d) { return (d.mac === selectedMac); });
var filteredDevices = (!selectedMac) ? stats.d : stats.d.filter(function (d) { return (d.mac === selectedMac); });
// 应用排序
filteredDevices = sortDevices(filteredDevices, currentSortBy, currentSortOrder);
// 检查是否有任何设备有 IPv6 地址
var hasAnyIPv6 = filteredDevices.some(function (device) {
var lanIPv6 = filterLanIPv6(device.ipv6_addresses);
var lanIPv6 = filterLanIPv6(device.ip6);
return lanIPv6.length > 0;
});
@ -5802,20 +5928,21 @@ return view.extend({
var deviceMode = isMobileScreen ? 'simple' : (localStorage.getItem('bandix_device_mode') || 'simple');
var isDetailedMode = deviceMode === 'detailed';
// 构建设备信息元素
var uplinkChBadges = buildDeviceUplinkChBadges(device);
var deviceInfoElements = [
E('div', { 'class': 'device-name' }, [
E('span', {
'class': 'device-status ' + (isOnline ? 'online' : 'offline')
}),
device.hostname || '-'
device.host || '-'
]),
E('div', { 'class': 'device-ip' }, [
device.connection_type ? E('span', {
device.conn ? E('span', {
'class': 'device-connection-type',
'title': device.connection_type === 'wifi' ? _('Wireless') : (device.connection_type === 'router' ? _('Router') : _('Wired'))
}, getConnectionTypeIcon(device.connection_type)) : '',
device.ip
'title': device.conn === 'wifi' ? _('Wireless') : (device.conn === 'router' ? _('Router') : _('Wired'))
}, getConnectionTypeIcon(device.conn)) : '',
device.ip4,
uplinkChBadges.length ? E('span', { 'class': 'device-uplink-badges' }, uplinkChBadges) : ''
])
];
@ -5823,9 +5950,9 @@ return view.extend({
if (isDetailedMode) {
// 只有当有设备有 IPv6 时才添加 IPv6 行
if (hasAnyIPv6) {
var lanIPv6 = filterLanIPv6(device.ipv6_addresses);
var lanIPv6 = filterLanIPv6(device.ip6);
if (lanIPv6.length > 0) {
var allIPv6 = device.ipv6_addresses ? device.ipv6_addresses.join(', ') : '';
var allIPv6 = device.ip6 ? device.ip6.join(', ') : '';
deviceInfoElements.push(E('div', {
'class': 'device-ipv6',
'title': allIPv6
@ -5840,8 +5967,8 @@ return view.extend({
E('div', { 'class': 'device-mac' }, device.mac),
E('div', { 'class': 'device-last-online' }, [
E('span', {}, _('Last Online') + ': '),
E('span', { 'class': 'device-last-online-value' }, formatLastOnlineTime(device.last_online_ts)),
E('span', { 'class': 'device-last-online-exact' }, formatLastOnlineExactTime(device.last_online_ts))
E('span', { 'class': 'device-last-online-value' }, formatLastOnlineTime(device.last)),
E('span', { 'class': 'device-last-online-exact' }, formatLastOnlineExactTime(device.last))
])
);
}
@ -5857,13 +5984,13 @@ return view.extend({
E('div', { 'class': 'traffic-info' }, [
E('div', { 'class': 'traffic-row' }, [
E('span', { 'class': 'traffic-icon upload' }, '↑'),
E('span', { 'class': 'traffic-speed lan' }, formatByterate(device.lan_tx_rate || 0, speedUnit)),
E('span', { 'class': 'traffic-total' }, '(' + formatSize(device.lan_tx_bytes || 0) + ')')
E('span', { 'class': 'traffic-speed lan' }, formatByterate(device.l_tx_r || 0, speedUnit)),
E('span', { 'class': 'traffic-total' }, '(' + formatSize(device.l_tx_b || 0) + ')')
]),
E('div', { 'class': 'traffic-row' }, [
E('span', { 'class': 'traffic-icon download' }, '↓'),
E('span', { 'class': 'traffic-speed lan' }, formatByterate(device.lan_rx_rate || 0, speedUnit)),
E('span', { 'class': 'traffic-total' }, '(' + formatSize(device.lan_rx_bytes || 0) + ')')
E('span', { 'class': 'traffic-speed lan' }, formatByterate(device.l_rx_r || 0, speedUnit)),
E('span', { 'class': 'traffic-total' }, '(' + formatSize(device.l_rx_b || 0) + ')')
])
])
]),
@ -5873,13 +6000,13 @@ return view.extend({
E('div', { 'class': 'traffic-info' }, [
E('div', { 'class': 'traffic-row' }, [
E('span', { 'class': 'traffic-icon upload' }, '↑'),
E('span', { 'class': 'traffic-speed wan' }, formatByterate(device.wan_tx_rate || 0, speedUnit)),
E('span', { 'class': 'traffic-total' }, '(' + formatSize(device.wan_tx_bytes || 0) + ')')
E('span', { 'class': 'traffic-speed wan' }, formatByterate(device.w_tx_r || 0, speedUnit)),
E('span', { 'class': 'traffic-total' }, '(' + formatSize(device.w_tx_b || 0) + ')')
]),
E('div', { 'class': 'traffic-row' }, [
E('span', { 'class': 'traffic-icon download' }, '↓'),
E('span', { 'class': 'traffic-speed wan' }, formatByterate(device.wan_rx_rate || 0, speedUnit)),
E('span', { 'class': 'traffic-total' }, '(' + formatSize(device.wan_rx_bytes || 0) + ')')
E('span', { 'class': 'traffic-speed wan' }, formatByterate(device.w_rx_r || 0, speedUnit)),
E('span', { 'class': 'traffic-total' }, '(' + formatSize(device.w_rx_b || 0) + ')')
])
])
]),
@ -6040,13 +6167,17 @@ return view.extend({
E('div', { 'class': 'device-card-name' }, [
E('span', { 'class': 'device-status ' + (isOnline ? 'online' : 'offline') }),
E('div', {}, [
E('div', { 'style': 'font-weight: 600;' }, device.hostname || '-'),
E('div', { 'style': 'font-weight: 600;' }, device.host || '-'),
E('div', { 'class': 'device-card-ip' }, [
device.connection_type ? E('span', {
device.conn ? E('span', {
'class': 'device-connection-type',
'title': device.connection_type === 'wifi' ? _('Wireless') : (device.connection_type === 'router' ? _('Router') : _('Wired'))
}, getConnectionTypeIcon(device.connection_type)) : '',
device.ip
'title': device.conn === 'wifi' ? _('Wireless') : (device.conn === 'router' ? _('Router') : _('Wired'))
}, getConnectionTypeIcon(device.conn)) : '',
device.ip4,
(function () {
var badges = buildDeviceUplinkChBadges(device);
return badges.length ? E('span', { 'class': 'device-uplink-badges' }, badges) : '';
})()
])
])
]),
@ -6092,13 +6223,13 @@ return view.extend({
E('div', { 'class': 'device-card-traffic' }, [
E('div', { 'class': 'device-card-traffic-row' }, [
E('span', { 'style': 'color: ' + BANDIX_COLOR_UPLOAD + '; font-size: 0.75rem; font-weight: bold;' }, '↑'),
E('span', { 'style': 'font-weight: 600;' }, formatByterate(device.wan_tx_rate || 0, speedUnit)),
E('span', { 'style': 'font-size: 0.75rem; opacity: 0.7;' }, '(' + formatSize(device.wan_tx_bytes || 0) + ')')
E('span', { 'style': 'font-weight: 600;' }, formatByterate(device.w_tx_r || 0, speedUnit)),
E('span', { 'style': 'font-size: 0.75rem; opacity: 0.7;' }, '(' + formatSize(device.w_tx_b || 0) + ')')
]),
E('div', { 'class': 'device-card-traffic-row' }, [
E('span', { 'style': 'color: ' + BANDIX_COLOR_DOWNLOAD + '; font-size: 0.75rem; font-weight: bold;' }, '↓'),
E('span', { 'style': 'font-weight: 600;' }, formatByterate(device.wan_rx_rate || 0, speedUnit)),
E('span', { 'style': 'font-size: 0.75rem; opacity: 0.7;' }, '(' + formatSize(device.wan_rx_bytes || 0) + ')')
E('span', { 'style': 'font-weight: 600;' }, formatByterate(device.w_rx_r || 0, speedUnit)),
E('span', { 'style': 'font-size: 0.75rem; opacity: 0.7;' }, '(' + formatSize(device.w_rx_b || 0) + ')')
])
])
])
@ -6153,13 +6284,13 @@ return view.extend({
E('div', { 'class': 'device-card-traffic' }, [
E('div', { 'class': 'device-card-traffic-row' }, [
E('span', { 'style': 'color: ' + BANDIX_COLOR_UPLOAD + '; font-size: 0.75rem; font-weight: bold;' }, '↑'),
E('span', { 'style': 'font-weight: 600;' }, formatByterate(device.lan_tx_rate || 0, speedUnit)),
E('span', { 'style': 'font-size: 0.75rem; opacity: 0.7;' }, '(' + formatSize(device.lan_tx_bytes || 0) + ')')
E('span', { 'style': 'font-weight: 600;' }, formatByterate(device.l_tx_r || 0, speedUnit)),
E('span', { 'style': 'font-size: 0.75rem; opacity: 0.7;' }, '(' + formatSize(device.l_tx_b || 0) + ')')
]),
E('div', { 'class': 'device-card-traffic-row' }, [
E('span', { 'style': 'color: ' + BANDIX_COLOR_DOWNLOAD + '; font-size: 0.75rem; font-weight: bold;' }, '↓'),
E('span', { 'style': 'font-weight: 600;' }, formatByterate(device.lan_rx_rate || 0, speedUnit)),
E('span', { 'style': 'font-size: 0.75rem; opacity: 0.7;' }, '(' + formatSize(device.lan_rx_bytes || 0) + ')')
E('span', { 'style': 'font-weight: 600;' }, formatByterate(device.l_rx_r || 0, speedUnit)),
E('span', { 'style': 'font-size: 0.75rem; opacity: 0.7;' }, '(' + formatSize(device.l_rx_b || 0) + ')')
])
])
])
@ -6182,7 +6313,7 @@ return view.extend({
// 更新历史趋势中的设备下拉
try {
latestDevices = stats.devices || [];
latestDevices = stats.d || [];
updateDeviceOptions(latestDevices);
} catch (e) { }
});
@ -6233,33 +6364,33 @@ return view.extend({
displayData.forEach(function (item) {
var rankingItem = E('div', {
'class': 'usage-ranking-item',
'style': '--progress-width: ' + (item.percentage || 0) + '%;'
'style': '--progress-width: ' + (item.pct || 0) + '%;'
}, [
E('div', { 'class': 'usage-ranking-rank' }, (item.rank || '-')),
E('div', { 'class': 'usage-ranking-rank' }, (item.r || '-')),
E('div', { 'class': 'usage-ranking-info' }, [
E('div', { 'class': 'usage-ranking-device' }, [
E('div', { 'class': 'usage-ranking-name' }, item.hostname || item.ip || item.mac || '-'),
E('div', { 'class': 'usage-ranking-name' }, item.host || item.ip4 || item.mac || '-'),
E('div', { 'class': 'usage-ranking-meta' }, [
E('span', {}, item.ip || '-'),
E('span', {}, item.ip4 || '-'),
E('span', {}, item.mac || '-'),
E('span', { 'class': 'usage-ranking-meta-total' }, formatSize(item.total_bytes || 0))
E('span', { 'class': 'usage-ranking-meta-total' }, formatSize(item.t_b || 0))
])
]),
E('div', { 'class': 'usage-ranking-stats' }, [
E('div', { 'class': 'usage-ranking-traffic' }, [
E('span', { 'class': 'usage-ranking-traffic-item tx' }, [
E('span', { 'class': 'usage-ranking-traffic-arrow' }, '↑'),
E('span', {}, formatSize(item.tx_bytes || 0))
E('span', {}, formatSize(item.tx_b || 0))
]),
E('span', { 'class': 'usage-ranking-traffic-item rx' }, [
E('span', { 'class': 'usage-ranking-traffic-arrow' }, '↓'),
E('span', {}, formatSize(item.rx_bytes || 0))
E('span', {}, formatSize(item.rx_b || 0))
]),
E('span', { 'class': 'usage-ranking-traffic-item total' }, [
E('span', {}, formatSize(item.total_bytes || 0))
E('span', {}, formatSize(item.t_b || 0))
])
]),
E('div', { 'class': 'usage-ranking-percentage' }, (item.percentage || 0).toFixed(1) + '%')
E('div', { 'class': 'usage-ranking-percentage' }, (item.pct || 0).toFixed(1) + '%')
])
])
]);
@ -6312,25 +6443,25 @@ return view.extend({
// 获取设备用量排行
callGetTrafficUsageRanking(startMs, endMs, networkType).then(function (result) {
console.log('Query result:', result);
if (!result || !result.rankings) {
if (!result || !result.r) {
return;
}
usageRankingData = result.rankings;
usageRankingData = result.r;
// 更新时间范围显示(包含上下行流量和总流量)
var timeRangeEl = document.getElementById('usage-ranking-timerange');
if (timeRangeEl && result.start_ms && result.end_ms) {
var timeRangeText = formatTimeRange(result.start_ms, result.end_ms);
if (timeRangeEl && result.start && result.end) {
var timeRangeText = formatTimeRange(result.start, result.end);
var parts = [];
if (result.total_tx_bytes !== undefined && result.total_tx_bytes !== null) {
parts.push('↑' + formatSize(result.total_tx_bytes));
if (result.t_tx_b !== undefined && result.t_tx_b !== null) {
parts.push('↑' + formatSize(result.t_tx_b));
}
if (result.total_rx_bytes !== undefined && result.total_rx_bytes !== null) {
parts.push('↓' + formatSize(result.total_rx_bytes));
if (result.t_rx_b !== undefined && result.t_rx_b !== null) {
parts.push('↓' + formatSize(result.t_rx_b));
}
if (result.total_bytes !== undefined && result.total_bytes !== null) {
parts.push(formatSize(result.total_bytes));
if (result.t_b !== undefined && result.t_b !== null) {
parts.push(formatSize(result.t_b));
}
if (parts.length > 0) {
timeRangeText += ' · ' + parts.join(' · ');
@ -6344,8 +6475,8 @@ return view.extend({
} else {
// 如果还没有设备数据,先获取设备数据
callStatus().then(function (deviceResult) {
if (deviceResult && deviceResult.devices) {
latestDevices = deviceResult.devices;
if (deviceResult && deviceResult.d) {
latestDevices = deviceResult.d;
updateDeviceSelectForIncrements(latestDevices);
}
}).catch(function (err) {
@ -6374,18 +6505,18 @@ return view.extend({
if (!item) return null;
var tsMs = item.ts_ms;
if (!tsMs && item.start_ts_ms) tsMs = item.start_ts_ms;
if (!tsMs && item.end_ts_ms) tsMs = item.end_ts_ms;
if (!tsMs && item.start) tsMs = item.start;
if (!tsMs && item.end) tsMs = item.end;
var rxBytes = (item.rx_bytes !== undefined && item.rx_bytes !== null) ? item.rx_bytes : null;
var txBytes = (item.tx_bytes !== undefined && item.tx_bytes !== null) ? item.tx_bytes : null;
var totalBytes = (item.total_bytes !== undefined && item.total_bytes !== null) ? item.total_bytes : null;
if (rxBytes === null) {
rxBytes = (item.wan_rx_bytes_inc || 0) + (item.lan_rx_bytes_inc || 0);
rxBytes = (item.w_rx_b || 0) + (item.l_rx_b || 0);
}
if (txBytes === null) {
txBytes = (item.wan_tx_bytes_inc || 0) + (item.lan_tx_bytes_inc || 0);
txBytes = (item.w_tx_b || 0) + (item.l_tx_b || 0);
}
if (totalBytes === null) {
totalBytes = rxBytes + txBytes;
@ -6438,7 +6569,7 @@ return view.extend({
}
callGetTrafficUsageIncrements(startMs, endMs, selectedAggregation, selectedMac, selectedNetworkType).then(function (result) {
if (!result || !result.increments) {
if (!result || !result.inc) {
var container = document.getElementById('traffic-increments-container');
if (container) {
container.innerHTML = '<div class="loading-state">' + _('No data') + '</div>';
@ -6448,21 +6579,21 @@ return view.extend({
return;
}
var normalizedIncrements = normalizeTrafficIncrementsList(result.increments);
var normalizedIncrements = normalizeTrafficIncrementsList(result.inc);
// 更新时间范围显示(包含上下行流量和总流量)
var timeRangeEl = document.getElementById('traffic-increments-timerange');
if (timeRangeEl && result.start_ms && result.end_ms) {
var timeRangeText = formatTimeRange(result.start_ms, result.end_ms);
if (timeRangeEl && result.start && result.end) {
var timeRangeText = formatTimeRange(result.start, result.end);
var parts = [];
if (result.total_tx_bytes !== undefined && result.total_tx_bytes !== null) {
parts.push('↑' + formatSize(result.total_tx_bytes));
if (result.t_tx_b !== undefined && result.t_tx_b !== null) {
parts.push('↑' + formatSize(result.t_tx_b));
}
if (result.total_rx_bytes !== undefined && result.total_rx_bytes !== null) {
parts.push('↓' + formatSize(result.total_rx_bytes));
if (result.t_rx_b !== undefined && result.t_rx_b !== null) {
parts.push('↓' + formatSize(result.t_rx_b));
}
if (result.total_bytes !== undefined && result.total_bytes !== null) {
parts.push(formatSize(result.total_bytes));
if (result.t_b !== undefined && result.t_b !== null) {
parts.push(formatSize(result.t_b));
}
if (parts.length > 0) {
timeRangeText += ' · ' + parts.join(' · ');
@ -6507,15 +6638,15 @@ return view.extend({
var summary = E('div', { 'class': 'traffic-increments-summary' }, [
E('div', { 'class': 'traffic-increments-summary-item' }, [
E('div', { 'class': 'traffic-increments-summary-label' }, _('Total Upload')),
E('div', { 'class': 'traffic-increments-summary-value' }, formatSize(result.total_tx_bytes || 0))
E('div', { 'class': 'traffic-increments-summary-value' }, formatSize(result.t_tx_b || 0))
]),
E('div', { 'class': 'traffic-increments-summary-item' }, [
E('div', { 'class': 'traffic-increments-summary-label' }, _('Total Download')),
E('div', { 'class': 'traffic-increments-summary-value' }, formatSize(result.total_rx_bytes || 0))
E('div', { 'class': 'traffic-increments-summary-value' }, formatSize(result.t_rx_b || 0))
]),
E('div', { 'class': 'traffic-increments-summary-item' }, [
E('div', { 'class': 'traffic-increments-summary-label' }, _('Total')),
E('div', { 'class': 'traffic-increments-summary-value' }, formatSize(result.total_bytes || 0))
E('div', { 'class': 'traffic-increments-summary-value' }, formatSize(result.t_b || 0))
])
]);
@ -6526,7 +6657,7 @@ return view.extend({
// 绘制图表
setTimeout(function () {
var aggregation = result.aggregation || 'hourly';
var aggregation = result.agg || 'hourly';
// 重置缩放状态
if (incrementsZoomTimer) {
@ -6639,8 +6770,8 @@ return view.extend({
if (!aOnline && bOnline) return 1;
// 在线状态相同时按IP地址排序
var aIp = a.ip || '';
var bIp = b.ip || '';
var aIp = a.ip4 || '';
var bIp = b.ip4 || '';
// 将IP地址转换为数字进行比较
var aIpParts = aIp.split('.').map(function (part) { return parseInt(part) || 0; });
@ -6662,7 +6793,7 @@ return view.extend({
// 添加设备选项
sortedDevices.forEach(function (device) {
if (device.mac) {
var label = (device.hostname || device.ip || device.mac || '-') + (device.ip ? ' (' + device.ip + ')' : '') + (device.mac ? ' [' + device.mac + ']' : '');
var label = (device.host || device.ip4 || device.mac || '-') + (device.ip4 ? ' (' + device.ip4 + ')' : '') + (device.mac ? ' [' + device.mac + ']' : '');
var option = E('option', { 'value': device.mac }, label);
macSelect.appendChild(option);
}
@ -6897,7 +7028,7 @@ return view.extend({
} else {
// 按小时聚合:显示整点小时
// 使用 start_ts_ms 来确定这个时间段代表哪个小时
var startTs = item.start_ts_ms || item.ts_ms;
var startTs = item.start || item.ts_ms;
var startDate = new Date(startTs);
// 使用开始时间的整点小时
@ -7053,7 +7184,7 @@ return view.extend({
}
var item = originalIncrements[barIndex];
var timeStr = formatTimeRange(item.start_ts_ms || item.ts_ms, item.end_ts_ms || item.ts_ms, isDaily);
var timeStr = formatTimeRange(item.start || item.ts_ms, item.end || item.ts_ms, isDaily);
// Get speed unit from UCI config
var speedUnit = uci.get('bandix', 'traffic', 'speed_unit') || 'bytes';
@ -7071,25 +7202,25 @@ return view.extend({
'<div class="ht-kpis">' +
'<div class="ht-kpi up">' +
'<div class="ht-k-label">WAN Upload</div>' +
'<div class="ht-k-value">' + formatSize(item.wan_tx_bytes_inc || 0) + '</div>' +
'<div class="ht-k-value">' + formatSize(item.w_tx_b || 0) + '</div>' +
'</div>' +
'<div class="ht-kpi down">' +
'<div class="ht-k-label">WAN Download</div>' +
'<div class="ht-k-value">' + formatSize(item.wan_rx_bytes_inc || 0) + '</div>' +
'<div class="ht-k-value">' + formatSize(item.w_rx_b || 0) + '</div>' +
'</div>' +
'</div>' +
// 速度统计分组
'<div class="ht-divider"></div>' +
'<div class="ht-section-title">' + _('Upload Statistics') + '</div>' +
'<div class="ht-row"><span class="ht-key">' + _('Average') + '</span><span class="ht-val">' + formatByterate(item.wan_tx_rate_avg || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">P95</span><span class="ht-val">' + formatByterate(item.wan_tx_rate_p95 || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">' + _('Maximum') + '</span><span class="ht-val">' + formatByterate(item.wan_tx_rate_max || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">' + _('Average') + '</span><span class="ht-val">' + formatByterate(item.w_tx_avg || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">P95</span><span class="ht-val">' + formatByterate(item.w_tx_p95 || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">' + _('Maximum') + '</span><span class="ht-val">' + formatByterate(item.w_tx_max || 0, speedUnit) + '</span></div>' +
'<div class="ht-section-title" style="margin-top: 8px;">' + _('Download Statistics') + '</div>' +
'<div class="ht-row"><span class="ht-key">' + _('Average') + '</span><span class="ht-val">' + formatByterate(item.wan_rx_rate_avg || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">P95</span><span class="ht-val">' + formatByterate(item.wan_rx_rate_p95 || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">' + _('Maximum') + '</span><span class="ht-val">' + formatByterate(item.wan_rx_rate_max || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">' + _('Average') + '</span><span class="ht-val">' + formatByterate(item.w_rx_avg || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">P95</span><span class="ht-val">' + formatByterate(item.w_rx_p95 || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">' + _('Maximum') + '</span><span class="ht-val">' + formatByterate(item.w_rx_max || 0, speedUnit) + '</span></div>' +
'</div>';
} else if (networkType === 'lan') {
tooltipContent +=
@ -7101,25 +7232,25 @@ return view.extend({
'<div class="ht-kpis">' +
'<div class="ht-kpi up">' +
'<div class="ht-k-label">LAN Upload</div>' +
'<div class="ht-k-value">' + formatSize(item.lan_tx_bytes_inc || 0) + '</div>' +
'<div class="ht-k-value">' + formatSize(item.l_tx_b || 0) + '</div>' +
'</div>' +
'<div class="ht-kpi down">' +
'<div class="ht-k-label">LAN Download</div>' +
'<div class="ht-k-value">' + formatSize(item.lan_rx_bytes_inc || 0) + '</div>' +
'<div class="ht-k-value">' + formatSize(item.l_rx_b || 0) + '</div>' +
'</div>' +
'</div>' +
// 速度统计分组
'<div class="ht-divider"></div>' +
'<div class="ht-section-title">' + _('Upload Statistics') + '</div>' +
'<div class="ht-row"><span class="ht-key">' + _('Average') + '</span><span class="ht-val">' + formatByterate(item.lan_tx_rate_avg || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">P95</span><span class="ht-val">' + formatByterate(item.lan_tx_rate_p95 || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">' + _('Maximum') + '</span><span class="ht-val">' + formatByterate(item.lan_tx_rate_max || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">' + _('Average') + '</span><span class="ht-val">' + formatByterate(item.l_tx_avg || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">P95</span><span class="ht-val">' + formatByterate(item.l_tx_p95 || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">' + _('Maximum') + '</span><span class="ht-val">' + formatByterate(item.l_tx_max || 0, speedUnit) + '</span></div>' +
'<div class="ht-section-title" style="margin-top: 8px;">' + _('Download Statistics') + '</div>' +
'<div class="ht-row"><span class="ht-key">' + _('Average') + '</span><span class="ht-val">' + formatByterate(item.lan_rx_rate_avg || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">P95</span><span class="ht-val">' + formatByterate(item.lan_rx_rate_p95 || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">' + _('Maximum') + '</span><span class="ht-val">' + formatByterate(item.lan_rx_rate_max || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">' + _('Average') + '</span><span class="ht-val">' + formatByterate(item.l_rx_avg || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">P95</span><span class="ht-val">' + formatByterate(item.l_rx_p95 || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">' + _('Maximum') + '</span><span class="ht-val">' + formatByterate(item.l_rx_max || 0, speedUnit) + '</span></div>' +
'</div>';
} else {
// networkType === 'all' 或其他情况显示所有section
@ -7132,25 +7263,25 @@ return view.extend({
'<div class="ht-kpis">' +
'<div class="ht-kpi up">' +
'<div class="ht-k-label">WAN Upload</div>' +
'<div class="ht-k-value">' + formatSize(item.wan_tx_bytes_inc || 0) + '</div>' +
'<div class="ht-k-value">' + formatSize(item.w_tx_b || 0) + '</div>' +
'</div>' +
'<div class="ht-kpi down">' +
'<div class="ht-k-label">WAN Download</div>' +
'<div class="ht-k-value">' + formatSize(item.wan_rx_bytes_inc || 0) + '</div>' +
'<div class="ht-k-value">' + formatSize(item.w_rx_b || 0) + '</div>' +
'</div>' +
'</div>' +
// 速度统计分组
'<div class="ht-divider"></div>' +
'<div class="ht-section-title">' + _('Upload Statistics') + '</div>' +
'<div class="ht-row"><span class="ht-key">' + _('Average') + '</span><span class="ht-val">' + formatByterate(item.wan_tx_rate_avg || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">P95</span><span class="ht-val">' + formatByterate(item.wan_tx_rate_p95 || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">' + _('Maximum') + '</span><span class="ht-val">' + formatByterate(item.wan_tx_rate_max || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">' + _('Average') + '</span><span class="ht-val">' + formatByterate(item.w_tx_avg || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">P95</span><span class="ht-val">' + formatByterate(item.w_tx_p95 || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">' + _('Maximum') + '</span><span class="ht-val">' + formatByterate(item.w_tx_max || 0, speedUnit) + '</span></div>' +
'<div class="ht-section-title" style="margin-top: 8px;">' + _('Download Statistics') + '</div>' +
'<div class="ht-row"><span class="ht-key">' + _('Average') + '</span><span class="ht-val">' + formatByterate(item.wan_rx_rate_avg || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">P95</span><span class="ht-val">' + formatByterate(item.wan_rx_rate_p95 || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">' + _('Maximum') + '</span><span class="ht-val">' + formatByterate(item.wan_rx_rate_max || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">' + _('Average') + '</span><span class="ht-val">' + formatByterate(item.w_rx_avg || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">P95</span><span class="ht-val">' + formatByterate(item.w_rx_p95 || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">' + _('Maximum') + '</span><span class="ht-val">' + formatByterate(item.w_rx_max || 0, speedUnit) + '</span></div>' +
'</div>' +
// LAN Traffic Section
@ -7161,25 +7292,25 @@ return view.extend({
'<div class="ht-kpis">' +
'<div class="ht-kpi up">' +
'<div class="ht-k-label">LAN Upload</div>' +
'<div class="ht-k-value">' + formatSize(item.lan_tx_bytes_inc || 0) + '</div>' +
'<div class="ht-k-value">' + formatSize(item.l_tx_b || 0) + '</div>' +
'</div>' +
'<div class="ht-kpi down">' +
'<div class="ht-k-label">LAN Download</div>' +
'<div class="ht-k-value">' + formatSize(item.lan_rx_bytes_inc || 0) + '</div>' +
'<div class="ht-k-value">' + formatSize(item.l_rx_b || 0) + '</div>' +
'</div>' +
'</div>' +
// 速度统计分组
'<div class="ht-divider"></div>' +
'<div class="ht-section-title">' + _('Upload Statistics') + '</div>' +
'<div class="ht-row"><span class="ht-key">' + _('Average') + '</span><span class="ht-val">' + formatByterate(item.lan_tx_rate_avg || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">P95</span><span class="ht-val">' + formatByterate(item.lan_tx_rate_p95 || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">' + _('Maximum') + '</span><span class="ht-val">' + formatByterate(item.lan_tx_rate_max || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">' + _('Average') + '</span><span class="ht-val">' + formatByterate(item.l_tx_avg || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">P95</span><span class="ht-val">' + formatByterate(item.l_tx_p95 || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">' + _('Maximum') + '</span><span class="ht-val">' + formatByterate(item.l_tx_max || 0, speedUnit) + '</span></div>' +
'<div class="ht-section-title" style="margin-top: 8px;">' + _('Download Statistics') + '</div>' +
'<div class="ht-row"><span class="ht-key">' + _('Average') + '</span><span class="ht-val">' + formatByterate(item.lan_rx_rate_avg || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">P95</span><span class="ht-val">' + formatByterate(item.lan_rx_rate_p95 || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">' + _('Maximum') + '</span><span class="ht-val">' + formatByterate(item.lan_rx_rate_max || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">' + _('Average') + '</span><span class="ht-val">' + formatByterate(item.l_rx_avg || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">P95</span><span class="ht-val">' + formatByterate(item.l_rx_p95 || 0, speedUnit) + '</span></div>' +
'<div class="ht-row"><span class="ht-key">' + _('Maximum') + '</span><span class="ht-val">' + formatByterate(item.l_rx_max || 0, speedUnit) + '</span></div>' +
'</div>';
}

View File

@ -689,6 +689,24 @@ return view.extend({
o.rmempty = false;
o.depends('traffic_enable_storage', '1');
o = s.option(form.Flag, 'traffic_neighbor_flush_enable', _('Enable Neighbor Flush'),
_('Enable periodic flush of neighbor table data. Generally not recommended if the device list works normally.'));
o.default = '0';
o.rmempty = false;
o = s.option(form.ListValue, 'traffic_neighbor_flush_interval', _('Neighbor Flush Interval'),
_('Set the interval for flushing neighbor table data'));
o.value('600', _('10 minutes'));
o.value('900', _('15 minutes'));
o.value('1800', _('30 minutes'));
o.value('3600', _('1 hour'));
o.value('7200', _('2 hours'));
o.value('43200', _('12 hours'));
o.value('86400', _('24 hours'));
o.default = '600';
o.rmempty = false;
o.depends('traffic_neighbor_flush_enable', '1');
// 添加历史流量周期(秒)
o = s.option(form.ListValue, 'traffic_realtime_window', _('Realtime Traffic Period'),
_('Does not occupy storage space, stored only in memory'));

View File

@ -1163,8 +1163,65 @@ msgstr "Excluir dispositivo de interfaz"
msgid "Exclude the router itself (interface device) from traffic statistics"
msgstr "Excluir el propio router (dispositivo de interfaz) de las estadísticas de tráfico"
msgid "Enable Neighbor Flush"
msgstr "Habilitar vaciado de tabla de vecinos"
msgid "Enable periodic flush of neighbor table data. Generally not recommended if the device list works normally."
msgstr "Habilitar vaciado periódico de datos de la tabla de vecinos. Generalmente no se recomienda si la lista de dispositivos funciona con normalidad."
msgid "Neighbor Flush Interval"
msgstr "Intervalo de vaciado de tabla de vecinos"
msgid "Set the interval for flushing neighbor table data"
msgstr "Establecer el intervalo de vaciado de datos de la tabla de vecinos"
msgid "No devices"
msgstr "Sin dispositivos"
msgid "LAN Traffic is viewed from the entire subnet perspective, so upload and download are always equal, forming a closed loop. Additionally, due to the presence of dedicated hardware switching chips, LAN traffic between 2 devices (e.g., PC - NAS) within the local network may not be monitorable."
msgstr "El tráfico LAN se ve desde la perspectiva de toda la subred, por lo que la carga y descarga siempre son iguales, formando un bucle cerrado. Además, debido a la presencia de chips de conmutación de hardware dedicados, el tráfico LAN entre 2 dispositivos (p. ej., PC - NAS) dentro de la red local puede no ser monitoreable."
msgid "Protocol"
msgstr "Protocolo"
msgid "State"
msgstr "Estado"
msgid "Send"
msgstr "Enviar"
msgid "Reply"
msgstr "Respuesta"
msgid "Flags"
msgstr "Marcas"
msgid "Loading…"
msgstr "Cargando…"
msgid "No connections"
msgstr "Sin conexiones"
msgid "pkts"
msgstr "paquetes"
msgid "Failed to load"
msgstr "Error al cargar"
msgid "Connection Details"
msgstr "Detalles de conexión"
msgid "Details"
msgstr "Detalles"
msgid "Orig Src"
msgstr "Origen orig."
msgid "Orig Dst"
msgstr "Destino orig."
msgid "Repl Src"
msgstr "Origen resp."
msgid "Repl Dst"
msgstr "Destino resp."

View File

@ -1163,8 +1163,65 @@ msgstr "Exclure l'appareil d'interface"
msgid "Exclude the router itself (interface device) from traffic statistics"
msgstr "Exclure le routeur lui-même (appareil d'interface) des statistiques de trafic"
msgid "Enable Neighbor Flush"
msgstr "Activer la vidange de la table des voisins"
msgid "Enable periodic flush of neighbor table data. Generally not recommended if the device list works normally."
msgstr "Activer le vidage périodique des données de la table des voisins. Généralement non recommandé si la liste des appareils fonctionne normalement."
msgid "Neighbor Flush Interval"
msgstr "Intervalle de vidange de la table des voisins"
msgid "Set the interval for flushing neighbor table data"
msgstr "Définir l'intervalle de vidange des données de la table des voisins"
msgid "No devices"
msgstr "Aucun appareil"
msgid "LAN Traffic is viewed from the entire subnet perspective, so upload and download are always equal, forming a closed loop. Additionally, due to the presence of dedicated hardware switching chips, LAN traffic between 2 devices (e.g., PC - NAS) within the local network may not be monitorable."
msgstr "Le trafic LAN est vu depuis la perspective de tout le sous-réseau, donc l'upload et le download sont toujours égaux, formant une boucle fermée. De plus, en raison de la présence de puces de commutation matérielles dédiées, le trafic LAN entre 2 appareils (par ex., PC - NAS) au sein du réseau local peut ne pas être surveillable."
msgstr "Le trafic LAN est vu depuis la perspective de tout le sous-réseau, donc l'upload et le download sont toujours égaux, formant une boucle fermée. De plus, en raison de la présence de puces de commutation matérielles dédiées, le trafic LAN entre 2 appareils (par ex., PC - NAS) au sein du réseau local peut ne pas être surveillable."
msgid "Protocol"
msgstr "Protocole"
msgid "State"
msgstr "État"
msgid "Send"
msgstr "Envoyer"
msgid "Reply"
msgstr "Réponse"
msgid "Flags"
msgstr "Drapeaux"
msgid "Loading…"
msgstr "Chargement…"
msgid "No connections"
msgstr "Aucune connexion"
msgid "pkts"
msgstr "paquets"
msgid "Failed to load"
msgstr "Échec du chargement"
msgid "Connection Details"
msgstr "Détails de connexion"
msgid "Details"
msgstr "Détails"
msgid "Orig Src"
msgstr "Src. orig."
msgid "Orig Dst"
msgstr "Dest. orig."
msgid "Repl Src"
msgstr "Src. répl."
msgid "Repl Dst"
msgstr "Dest. répl."

View File

@ -1163,8 +1163,65 @@ msgstr "インターフェースデバイスを除外"
msgid "Exclude the router itself (interface device) from traffic statistics"
msgstr "トラフィック統計からルーター自体(インターフェースデバイス)を除外する"
msgid "Enable Neighbor Flush"
msgstr "ネイバーテーブルフラッシュを有効にする"
msgid "Enable periodic flush of neighbor table data. Generally not recommended if the device list works normally."
msgstr "ネイバーテーブルデータの定期フラッシュを有効にします。デバイスリストが正常に動作している場合は、通常は有効にしないことをお勧めします。"
msgid "Neighbor Flush Interval"
msgstr "ネイバーテーブルフラッシュ間隔"
msgid "Set the interval for flushing neighbor table data"
msgstr "ネイバーテーブルデータのフラッシュ間隔を設定"
msgid "No devices"
msgstr "デバイスなし"
msgid "LAN Traffic is viewed from the entire subnet perspective, so upload and download are always equal, forming a closed loop. Additionally, due to the presence of dedicated hardware switching chips, LAN traffic between 2 devices (e.g., PC - NAS) within the local network may not be monitorable."
msgstr "LANトラフィックはサブネット全体の視点から見られるため、アップロードとダウンロードは常に等しく、閉ループを形成します。さらに、専用ハードウェアスイッチングチップの存在により、ローカルネットワーク内の2つのデバイスPC - NAS間のLANトラフィックは監視できない可能性があります。"
msgid "Protocol"
msgstr "プロトコル"
msgid "State"
msgstr "状態"
msgid "Send"
msgstr "送信"
msgid "Reply"
msgstr "応答"
msgid "Flags"
msgstr "フラグ"
msgid "Loading…"
msgstr "読み込み中…"
msgid "No connections"
msgstr "接続なし"
msgid "pkts"
msgstr "パケット"
msgid "Failed to load"
msgstr "読み込み失敗"
msgid "Connection Details"
msgstr "接続詳細"
msgid "Details"
msgstr "詳細"
msgid "Orig Src"
msgstr "送信元"
msgid "Orig Dst"
msgstr "送信先"
msgid "Repl Src"
msgstr "応答元"
msgid "Repl Dst"
msgstr "応答先"

View File

@ -1163,8 +1163,65 @@ msgstr "Wyklucz urządzenie interfejsu"
msgid "Exclude the router itself (interface device) from traffic statistics"
msgstr "Wyklucz sam router (urządzenie interfejsu) ze statystyk ruchu"
msgid "Enable Neighbor Flush"
msgstr "Włącz odświeżanie tabeli sąsiadów"
msgid "Enable periodic flush of neighbor table data. Generally not recommended if the device list works normally."
msgstr "Włącz okresowe odświeżanie danych tabeli sąsiadów. Ogólnie nie zalecane, jeśli lista urządzeń działa prawidłowo."
msgid "Neighbor Flush Interval"
msgstr "Interwał odświeżania tabeli sąsiadów"
msgid "Set the interval for flushing neighbor table data"
msgstr "Ustaw interwał odświeżania danych tabeli sąsiadów"
msgid "No devices"
msgstr "Brak urządzeń"
msgid "LAN Traffic is viewed from the entire subnet perspective, so upload and download are always equal, forming a closed loop. Additionally, due to the presence of dedicated hardware switching chips, LAN traffic between 2 devices (e.g., PC - NAS) within the local network may not be monitorable."
msgstr "Ruch LAN jest postrzegany z perspektywy całej podsieci, więc przesyłanie i pobieranie są zawsze równe, tworząc zamkniętą pętlę. Ponadto, ze względu na obecność dedykowanych chipów przełączających sprzętowych, ruch LAN między 2 urządzeniami (np. PC - NAS) w sieci lokalnej może nie być monitorowany."
msgid "Protocol"
msgstr "Protokół"
msgid "State"
msgstr "Stan"
msgid "Send"
msgstr "Wyślij"
msgid "Reply"
msgstr "Odpowiedź"
msgid "Flags"
msgstr "Flagi"
msgid "Loading…"
msgstr "Ładowanie…"
msgid "No connections"
msgstr "Brak połączeń"
msgid "pkts"
msgstr "pakiety"
msgid "Failed to load"
msgstr "Błąd ładowania"
msgid "Connection Details"
msgstr "Szczegóły połączenia"
msgid "Details"
msgstr "Szczegóły"
msgid "Orig Src"
msgstr "Źródło orig."
msgid "Orig Dst"
msgstr "Cel orig."
msgid "Repl Src"
msgstr "Źródło odp."
msgid "Repl Dst"
msgstr "Cel odp."

View File

@ -1163,8 +1163,65 @@ msgstr "Исключить устройство интерфейса"
msgid "Exclude the router itself (interface device) from traffic statistics"
msgstr "Исключить сам роутер (устройство интерфейса) из статистики трафика"
msgid "Enable Neighbor Flush"
msgstr "Включить сброс таблицы соседей"
msgid "Enable periodic flush of neighbor table data. Generally not recommended if the device list works normally."
msgstr "Включить периодический сброс данных таблицы соседей. Обычно не рекомендуется, если список устройств работает нормально."
msgid "Neighbor Flush Interval"
msgstr "Интервал сброса таблицы соседей"
msgid "Set the interval for flushing neighbor table data"
msgstr "Установить интервал сброса данных таблицы соседей"
msgid "No devices"
msgstr "Нет устройств"
msgid "LAN Traffic is viewed from the entire subnet perspective, so upload and download are always equal, forming a closed loop. Additionally, due to the presence of dedicated hardware switching chips, LAN traffic between 2 devices (e.g., PC - NAS) within the local network may not be monitorable."
msgstr "Трафик LAN рассматривается с точки зрения всей подсети, поэтому загрузка и выгрузка всегда равны, образуя замкнутый цикл. Кроме того, из-за наличия специализированных аппаратных коммутационных чипов, трафик LAN между 2 устройствами (например, ПК - NAS) в локальной сети может быть не подлежащим мониторингу."
msgstr "Трафик LAN рассматривается с точки зрения всей подсети, поэтому загрузка и выгрузка всегда равны, образуя замкнутый цикл. Кроме того, из-за наличия специализированных аппаратных коммутационных чипов, трафик LAN между 2 устройствами (например, ПК - NAS) в локальной сети может быть не подлежащим мониторингу."
msgid "Protocol"
msgstr "Протокол"
msgid "State"
msgstr "Состояние"
msgid "Send"
msgstr "Отправка"
msgid "Reply"
msgstr "Ответ"
msgid "Flags"
msgstr "Флаги"
msgid "Loading…"
msgstr "Загрузка…"
msgid "No connections"
msgstr "Нет подключений"
msgid "pkts"
msgstr "пакеты"
msgid "Failed to load"
msgstr "Ошибка загрузки"
msgid "Connection Details"
msgstr "Подробности соединения"
msgid "Details"
msgstr "Подробнее"
msgid "Orig Src"
msgstr "Ист. отпр."
msgid "Orig Dst"
msgstr "Назнач. отпр."
msgid "Repl Src"
msgstr "Ист. ответа"
msgid "Repl Dst"
msgstr "Назнач. ответа"

View File

@ -1190,8 +1190,65 @@ msgstr "排除接口设备"
msgid "Exclude the router itself (interface device) from traffic statistics"
msgstr "从流量统计中排除路由器本身(接口设备)"
msgid "Enable Neighbor Flush"
msgstr "启用邻居表刷新"
msgid "Enable periodic flush of neighbor table data. Generally not recommended if the device list works normally."
msgstr "启用定期刷新邻居表数据。若设备列表正常,通常不建议开启。"
msgid "Neighbor Flush Interval"
msgstr "邻居表刷新间隔"
msgid "Set the interval for flushing neighbor table data"
msgstr "设置邻居表数据刷新的时间间隔"
msgid "No devices"
msgstr "无设备"
msgid "LAN Traffic is viewed from the entire subnet perspective, so upload and download are always equal, forming a closed loop. Additionally, due to the presence of dedicated hardware switching chips, LAN traffic between 2 devices (e.g., PC - NAS) within the local network may not be monitorable."
msgstr "LAN 流量是从整个子网视角来看,所以上传和下载总是相等,是一个闭环。另外,由于专用硬件交换芯片的存在,局域网中 2 终端(比如 PC - NAS之间的 LAN 流量可能无法监控。"
msgstr "LAN 流量是从整个子网视角来看,所以上传和下载总是相等,是一个闭环。另外,由于专用硬件交换芯片的存在,局域网中 2 终端(比如 PC - NAS之间的 LAN 流量可能无法监控。"
msgid "Protocol"
msgstr "协议"
msgid "State"
msgstr "状态"
msgid "Send"
msgstr "发送"
msgid "Reply"
msgstr "回复"
msgid "Flags"
msgstr "标记"
msgid "Loading…"
msgstr "加载中…"
msgid "No connections"
msgstr "暂无连接"
msgid "pkts"
msgstr "包"
msgid "Failed to load"
msgstr "加载失败"
msgid "Connection Details"
msgstr "连接详情"
msgid "Details"
msgstr "详情"
msgid "Orig Src"
msgstr "发起源"
msgid "Orig Dst"
msgstr "发起目标"
msgid "Repl Src"
msgstr "回复源"
msgid "Repl Dst"
msgstr "回复目标"

View File

@ -1163,8 +1163,65 @@ msgstr "排除介面設備"
msgid "Exclude the router itself (interface device) from traffic statistics"
msgstr "從流量統計中排除路由器本身(介面設備)"
msgid "Enable Neighbor Flush"
msgstr "啟用鄰居表刷新"
msgid "Enable periodic flush of neighbor table data. Generally not recommended if the device list works normally."
msgstr "啟用定期刷新鄰居表數據。若設備列表正常,通常不建議開啟。"
msgid "Neighbor Flush Interval"
msgstr "鄰居表刷新間隔"
msgid "Set the interval for flushing neighbor table data"
msgstr "設置鄰居表數據刷新的時間間隔"
msgid "No devices"
msgstr "無設備"
msgid "LAN Traffic is viewed from the entire subnet perspective, so upload and download are always equal, forming a closed loop. Additionally, due to the presence of dedicated hardware switching chips, LAN traffic between 2 devices (e.g., PC - NAS) within the local network may not be monitorable."
msgstr "LAN 流量是從整個子網視角來看,所以上傳和下載總是相等,是一個閉環。另外,由於專用硬體交換晶片的存在,區域網路中 2 終端(比如 PC - NAS之間的 LAN 流量可能無法監控。"
msgstr "LAN 流量是從整個子網視角來看,所以上傳和下載總是相等,是一個閉環。另外,由於專用硬體交換晶片的存在,區域網路中 2 終端(比如 PC - NAS之間的 LAN 流量可能無法監控。"
msgid "Protocol"
msgstr "協定"
msgid "State"
msgstr "狀態"
msgid "Send"
msgstr "發送"
msgid "Reply"
msgstr "回覆"
msgid "Flags"
msgstr "標記"
msgid "Loading…"
msgstr "載入中…"
msgid "No connections"
msgstr "暫無連線"
msgid "pkts"
msgstr "封包"
msgid "Failed to load"
msgstr "載入失敗"
msgid "Connection Details"
msgstr "連線詳情"
msgid "Details"
msgstr "詳情"
msgid "Orig Src"
msgstr "發起源"
msgid "Orig Dst"
msgstr "發起目標"
msgid "Repl Src"
msgstr "回覆源"
msgid "Repl Dst"
msgstr "回覆目標"

View File

@ -10,6 +10,7 @@ readonly BANDIX_DEVICES_API="$BANDIX_API_BASE/api/traffic/devices"
readonly BANDIX_LIMITS_API="$BANDIX_API_BASE/api/traffic/limits"
readonly BANDIX_METRICS_API="$BANDIX_API_BASE/api/traffic/metrics"
readonly BANDIX_CONNECTION_API="$BANDIX_API_BASE/api/connection/devices"
readonly BANDIX_CONNECTION_FLOWS_API="$BANDIX_API_BASE/api/connection/flows"
readonly BANDIX_DNS_QUERIES_API="$BANDIX_API_BASE/api/dns/queries"
readonly BANDIX_DNS_STATS_API="$BANDIX_API_BASE/api/dns/stats"
readonly BANDIX_SCHEDULE_LIMITS_API="$BANDIX_API_BASE/api/traffic/limits/schedule"
@ -152,6 +153,26 @@ delete_device() {
}
# 获取设备状态
# GET /api/traffic/devices -> $.data
# {
# d: [{
# mac, # MAC 地址
# ip4, # IPv4 地址
# ip6, # IPv6 地址列表
# host, # 主机名
# conn, # 接入类型 (wifi/wired/router)
# uplink, # 接入接口名 (eth1/lan2/phy0-ap0),无则空串
# w_ch, # WiFi 频道号 (1/36/149),非 WiFi 为 0
# last, # 最后在线时间戳(ms)
# t_rx_b, t_tx_b, # 总下/上行字节
# t_rx_r, t_tx_r, # 总下/上行速率
# w_rx_b, w_tx_b, # WAN 下/上行字节
# w_rx_r, w_tx_r, # WAN 下/上行速率
# w_rx_l, w_tx_l, # WAN 下/上行限速
# l_rx_b, l_tx_b, # LAN 下/上行字节
# l_rx_r, l_tx_r # LAN 下/上行速率
# }]
# }
get_device_status() {
local url="$BANDIX_DEVICES_API"
local start_ms="$1"
@ -171,7 +192,7 @@ get_device_status() {
# 检查API调用是否成功
if [ $? -ne 0 ] || [ -z "$api_result" ]; then
echo '{"devices":[]}'
echo '{"d":[]}'
return
fi
@ -180,7 +201,7 @@ get_device_status() {
if [ -n "$data_part" ]; then
echo "$data_part"
else
echo '{"devices":[]}'
echo '{"d":[]}'
fi
}
@ -275,6 +296,25 @@ set_device_hostname() {
}
# 获取连接监控数据
# GET /api/connection/devices -> raw response (no $.data extraction)
# {
# status, data: {
# cnt, # 设备数量
# last, # 最后更新时间戳(ms)
# g: { # 全局连接统计
# total, # 总连接数
# tcp, udp, # TCP/UDP 连接数
# tcp_est, tcp_tw, tcp_cw, # ESTABLISHED/TIME_WAIT/CLOSE_WAIT
# last # 最后更新时间戳(ms)
# },
# d: [{ # 各设备连接统计
# mac, ip4, host, # 设备标识
# total, tcp, udp, # 总/TCP/UDP 连接数
# tcp_est, tcp_tw, tcp_cw, # ESTABLISHED/TIME_WAIT/CLOSE_WAIT
# last # 最后更新时间戳(ms)
# }]
# }
# }
get_connection_devices() {
# 检查连接监控是否启用
local connection_enabled=$(uci get bandix.connections.enabled 2>/dev/null)
@ -296,6 +336,34 @@ get_connection_devices() {
echo "$response"
}
get_connection_flows() {
local connection_enabled=$(uci get bandix.connections.enabled 2>/dev/null)
if [ "$connection_enabled" != "1" ]; then
make_error "Connection monitoring is not enabled"
return
fi
local ip="$1"
local protocol="$2"
local state="$3"
local query_params=""
[ -n "$ip" ] && query_params="${query_params}ip=$(printf '%s' "$ip" | sed 's/ /%20/g')&"
[ -n "$protocol" ] && query_params="${query_params}protocol=$(printf '%s' "$protocol" | sed 's/ /%20/g')&"
[ -n "$state" ] && query_params="${query_params}state=$(printf '%s' "$state" | sed 's/ /%20/g')&"
query_params=$(echo "$query_params" | sed 's/&$//')
local url="$BANDIX_CONNECTION_FLOWS_API"
[ -n "$query_params" ] && url="${url}?${query_params}"
local response=$(curl -s --connect-timeout 2 --max-time 5 -X GET "$url" 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$response" ]; then
make_error "Failed to connect to Bandix service"
return
fi
echo "$response"
}
# 获取 DNS 查询记录
get_dns_queries() {
# 检查 DNS 监控是否启用
@ -365,6 +433,19 @@ get_dns_stats() {
}
# 获取设备用量排行
# GET /api/traffic/usage/ranking -> $.data
# {
# start, end, # 统计时间范围(ms)
# net, # 网络类型 (wan/lan/all)
# cnt, # 设备数量
# t_b, t_rx_b, t_tx_b, # 总/下/上行字节
# r: [{
# mac, ip4, host, # 设备标识
# r, # 排名
# pct, # 占比(%)
# t_b, rx_b, tx_b # 总/下/上行字节
# }]
# }
get_traffic_usage_ranking() {
local start_ms="$1"
local end_ms="$2"
@ -406,11 +487,28 @@ get_traffic_usage_ranking() {
echo "$data_part"
else
# 如果提取失败,返回空结果
echo '{"start_ms":0,"end_ms":0,"total_rx_bytes":0,"total_tx_bytes":0,"total_bytes":0,"device_count":0,"rankings":[]}'
echo '{"start":0,"end":0,"t_rx_b":0,"t_tx_b":0,"t_b":0,"cnt":0,"r":[]}'
fi
}
# 获取时间序列增量数据
# GET /api/traffic/usage/increments -> $.data
# {
# start, end, # 统计时间范围(ms)
# agg, # 聚合粒度 (1m/5m/1h/1d)
# net, # 网络类型 (wan/lan/all)
# mac, # 指定设备 MAC空=全部)
# t_b, t_rx_b, t_tx_b, # 总/下/上行字节
# inc: [{
# start, end, # 分段时间范围(ms)
# w_rx_avg/max/min/p90/p95/p99, # WAN 下行速率统计
# w_tx_avg/max/min/p90/p95/p99, # WAN 上行速率统计
# l_rx_avg/max/min/p90/p95/p99, # LAN 下行速率统计
# l_tx_avg/max/min/p90/p95/p99, # LAN 上行速率统计
# w_rx_b, w_tx_b, # WAN 下/上行字节增量
# l_rx_b, l_tx_b # LAN 下/上行字节增量
# }]
# }
get_traffic_usage_increments() {
local start_ms="$1"
local end_ms="$2"
@ -462,7 +560,7 @@ get_traffic_usage_increments() {
echo "$data_part"
else
# 如果提取失败,返回空结果
echo '{"start_ms":0,"end_ms":0,"aggregation":"hourly","mac":"all","increments":[],"total_rx_bytes":0,"total_tx_bytes":0,"total_bytes":0}'
echo '{"start":0,"end":0,"agg":"hourly","mac":"all","inc":[],"t_rx_b":0,"t_tx_b":0,"t_b":0}'
fi
}
@ -1145,6 +1243,12 @@ case "$1" in
json_add_object "getConnection"
json_close_object
json_add_object "getConnectionFlows"
json_add_string "ip"
json_add_string "protocol"
json_add_string "state"
json_close_object
json_add_object "getDnsQueries"
json_add_string "domain"
@ -1336,6 +1440,28 @@ case "$1" in
# logger "luci.bandix: getConnection called"
get_connection_devices
;;
getConnectionFlows)
ip=""
protocol=""
state=""
input=""
if read -t 1 -r input; then
:
fi
if [ -n "$input" ]; then
ip="$(echo "$input" | jsonfilter -e '$[0]' 2>/dev/null)"
[ -z "$ip" ] && ip="$(echo "$input" | jsonfilter -e '$.ip' 2>/dev/null)"
protocol="$(echo "$input" | jsonfilter -e '$[1]' 2>/dev/null)"
[ -z "$protocol" ] && protocol="$(echo "$input" | jsonfilter -e '$.protocol' 2>/dev/null)"
state="$(echo "$input" | jsonfilter -e '$[2]' 2>/dev/null)"
[ -z "$state" ] && state="$(echo "$input" | jsonfilter -e '$.state' 2>/dev/null)"
else
[ -n "$3" ] && ip="$3"
[ -n "$4" ] && protocol="$4"
[ -n "$5" ] && state="$5"
fi
get_connection_flows "$ip" "$protocol" "$state"
;;
getDnsQueries)
# logger "luci.bandix: getDnsQueries called"
# 从 stdin 读取 JSON 参数

View File

@ -10,6 +10,7 @@
"getMetricsWeek",
"getMetricsMonth",
"getConnection",
"getConnectionFlows",
"setHostname",
"deleteDevice",
"getDnsQueries",
@ -41,6 +42,7 @@
"getMetricsWeek",
"getMetricsMonth",
"getConnection",
"getConnectionFlows",
"setHostname",
"deleteDevice",
"getDnsQueries",

View File

@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=bandix
PKG_VERSION:=0.12.6
PKG_RELEASE:=1
PKG_VERSION:=0.12.7
PKG_RELEASE:=2
PKG_LICENSE:=Apache-2.0
PKG_MAINTAINER:=timsaya
@ -13,7 +13,7 @@ include $(INCLUDE_DIR)/package.mk
include $(TOPDIR)/feeds/packages/lang/rust/rust-values.mk
# 二进制文件的文件名和URL
RUST_BANDIX_VERSION:=0.12.6
RUST_BANDIX_VERSION:=0.12.7
RUST_BINARY_FILENAME:=bandix-$(RUST_BANDIX_VERSION)-$(RUSTC_TARGET_ARCH).tar.gz
@ -69,7 +69,7 @@ define Package/$(PKG_NAME)
SECTION:=net
CATEGORY:=Network
TITLE:=Bandix - Network traffic monitoring tool
DEPENDS:=+zoneinfo-core +conntrack
DEPENDS:=+zoneinfo-all +conntrack +iw
endef
define Package/$(PKG_NAME)/description

View File

@ -22,6 +22,8 @@ config bandix 'traffic'
option traffic_event_url ''
option traffic_additional_subnets ''
option traffic_exclude_iface_device '0'
option traffic_neighbor_flush_enable '0'
option traffic_neighbor_flush_interval '600'
config bandix 'connections'

View File

@ -26,6 +26,8 @@ start_service() {
local traffic_event_url
local traffic_additional_subnets
local traffic_exclude_iface_device
local traffic_neighbor_flush_enable
local traffic_neighbor_flush_interval
local connections_enabled
local dns_enabled
local dns_max_records
@ -46,6 +48,8 @@ start_service() {
config_get traffic_event_url 'traffic' 'traffic_event_url'
config_get traffic_additional_subnets 'traffic' 'traffic_additional_subnets'
config_get_bool traffic_exclude_iface_device 'traffic' 'traffic_exclude_iface_device' '0'
config_get_bool traffic_neighbor_flush_enable 'traffic' 'traffic_neighbor_flush_enable' '0'
config_get traffic_neighbor_flush_interval 'traffic' 'traffic_neighbor_flush_interval' '600'
config_get_bool connections_enabled 'connections' 'enabled'
config_get_bool dns_enabled 'dns' 'enabled'
config_get dns_max_records 'dns' 'dns_max_records'
@ -89,6 +93,12 @@ start_service() {
if [ "$traffic_exclude_iface_device" -eq 1 ]; then
args="$args --traffic-exclude-iface-device"
fi
if [ "$traffic_neighbor_flush_enable" -eq 1 ]; then
args="$args --traffic-neighbor-flush-enable"
fi
if [ -n "$traffic_neighbor_flush_interval" ]; then
args="$args --traffic-neighbor-flush-interval $traffic_neighbor_flush_interval"
fi
# 添加连接统计监控参数
if [ "$connections_enabled" -eq 1 ]; then