mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-07-27 10:31:38 +08:00
🚀 Sync 2026-03-09 20:23:16
This commit is contained in:
parent
b99e3e42e7
commit
6541db9764
@ -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
|
||||
|
||||
|
||||
@ -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, {
|
||||
|
||||
@ -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>';
|
||||
}
|
||||
|
||||
|
||||
@ -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'));
|
||||
|
||||
@ -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."
|
||||
|
||||
@ -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."
|
||||
@ -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 "応答先"
|
||||
|
||||
@ -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."
|
||||
|
||||
@ -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 "Назнач. ответа"
|
||||
@ -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 "回复目标"
|
||||
@ -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 "回覆目標"
|
||||
@ -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 参数
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
"getMetricsWeek",
|
||||
"getMetricsMonth",
|
||||
"getConnection",
|
||||
"getConnectionFlows",
|
||||
"setHostname",
|
||||
"deleteDevice",
|
||||
"getDnsQueries",
|
||||
@ -41,6 +42,7 @@
|
||||
"getMetricsWeek",
|
||||
"getMetricsMonth",
|
||||
"getConnection",
|
||||
"getConnectionFlows",
|
||||
"setHostname",
|
||||
"deleteDevice",
|
||||
"getDnsQueries",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user