mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-03 01:13:56 +08:00
🐤 Sync 2026-10-27 12:12:04
This commit is contained in:
1 parent
8b63d5d4a9
commit
e646593c56
30 files changed
+1281
-416
No files matched your search
@@ -11,7 +11,7 @@ LUCI_DEPENDS:=+luci-base +luci-lib-jsonc +curl +bandix
|
|||||||
PKG_MAINTAINER:=timsaya
|
PKG_MAINTAINER:=timsaya
|
||||||
|
|
||||||
PKG_VERSION:=0.12.12
|
PKG_VERSION:=0.12.12
|
||||||
PKG_RELEASE:=14
|
PKG_RELEASE:=15
|
||||||
# Rebuild language packages when a package revision changes without a PO commit.
|
# Rebuild language packages when a package revision changes without a PO commit.
|
||||||
PKG_PO_VERSION=$(if $(DUMP),x,$(strip $(call findrev)))-r$(PKG_RELEASE)
|
PKG_PO_VERSION=$(if $(DUMP),x,$(strip $(call findrev)))-r$(PKG_RELEASE)
|
||||||
|
|
||||||
|
|||||||
@@ -18,14 +18,18 @@ function unwrap(reply) {
|
|||||||
if (!reply || reply.status !== 'success' || !reply.data) throw new Error(reply && (reply.message || reply.error) || _('GeoIP service is unavailable. Enable connection monitoring and apply settings, or update Bandix.'));
|
if (!reply || reply.status !== 'success' || !reply.data) throw new Error(reply && (reply.message || reply.error) || _('GeoIP service is unavailable. Enable connection monitoring and apply settings, or update Bandix.'));
|
||||||
return reply.data;
|
return reply.data;
|
||||||
}
|
}
|
||||||
|
function locale() {
|
||||||
|
// Standard LuCI exposes the active language on <html>, not in L.env.
|
||||||
|
return (L.env.lang || document.documentElement.lang || 'en').replace(/_/g, '-');
|
||||||
|
}
|
||||||
function localized(names) {
|
function localized(names) {
|
||||||
var language = (L.env.lang || document.documentElement.lang || 'en').replace(/_/g, '-');
|
var language = locale();
|
||||||
var key = /^zh/i.test(language) ? 'zh-CN' : language.split('-')[0];
|
var key = /^zh/i.test(language) ? 'zh-CN' : language.split('-')[0];
|
||||||
return names && (names[key] || names.en) || '';
|
return names && (names[key] || names.en) || '';
|
||||||
}
|
}
|
||||||
function countryName(info) {
|
function countryName(info) {
|
||||||
if (info.country_code && typeof Intl !== 'undefined' && Intl.DisplayNames) {
|
if (info.country_code && typeof Intl !== 'undefined' && Intl.DisplayNames) {
|
||||||
try { return new Intl.DisplayNames([(L.env.lang || 'en').replace(/_/g, '-')], { type: 'region' }).of(info.country_code); } catch (e) { /* use database names */ }
|
try { return new Intl.DisplayNames([locale()], { type: 'region' }).of(info.country_code); } catch (e) { /* use database names */ }
|
||||||
}
|
}
|
||||||
return localized(info.country_names) || info.country_code || '';
|
return localized(info.country_names) || info.country_code || '';
|
||||||
}
|
}
|
||||||
@@ -119,7 +123,7 @@ return baseclass.extend({
|
|||||||
var builds = databases.map(function (db) {
|
var builds = databases.map(function (db) {
|
||||||
var date = '—';
|
var date = '—';
|
||||||
if (db.build_epoch) {
|
if (db.build_epoch) {
|
||||||
try { date = new Date(db.build_epoch * 1000).toLocaleDateString((L.env.lang || 'en').replace(/_/g, '-')); }
|
try { date = new Date(db.build_epoch * 1000).toLocaleDateString(locale()); }
|
||||||
catch (e) { date = new Date(db.build_epoch * 1000).toLocaleDateString(); }
|
catch (e) { date = new Date(db.build_epoch * 1000).toLocaleDateString(); }
|
||||||
}
|
}
|
||||||
return databaseName(db.kind) + ': ' + date;
|
return databaseName(db.kind) + ': ' + date;
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
'use strict';
|
||||||
|
'require baseclass';
|
||||||
|
'require rpc';
|
||||||
|
|
||||||
|
var callSave = rpc.declare({ object: 'luci.bandix', method: 'setTrafficQuotaConfig', params: ['quota'], expect: {} });
|
||||||
|
var periods = [
|
||||||
|
{ key: 'minute', label: _('Minute') }, { key: 'hourly', label: _('Hourly') },
|
||||||
|
{ key: 'daily', label: _('Daily') }, { key: 'weekly', label: _('Weekly') },
|
||||||
|
{ key: 'monthly', label: _('Monthly') }, { key: 'total', label: _('Lifetime') }
|
||||||
|
];
|
||||||
|
var units = [1, 1024, 1048576, 1073741824, 1099511627776];
|
||||||
|
var unitNames = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
|
||||||
|
|
||||||
|
function modeOf(quota) { return quota && quota.mode === 'directional' ? 'directional' : 'combined'; }
|
||||||
|
function blockedText(quota) {
|
||||||
|
if (!quota || !quota.blocked) return '';
|
||||||
|
if (modeOf(quota) === 'combined' || !quota.upload || !quota.download || (quota.upload.blocked && quota.download.blocked)) return _('Upload and download blocked');
|
||||||
|
if (quota.upload.blocked) return _('Upload blocked');
|
||||||
|
if (quota.download.blocked) return _('Download blocked');
|
||||||
|
return _('Quota blocked');
|
||||||
|
}
|
||||||
|
function summary(quota, formatSize) {
|
||||||
|
if (!quota) return null;
|
||||||
|
var groups = modeOf(quota) === 'directional'
|
||||||
|
? [{ label: _('Upload'), data: quota.upload || {} }, { label: _('Download'), data: quota.download || {} }]
|
||||||
|
: [{ label: _('Combined'), data: quota }];
|
||||||
|
return E('div', { 'class': 'traffic-quota-cell', title: blockedText(quota) || _('Traffic Quota') }, groups.map(function (group) {
|
||||||
|
var mostUsed = null;
|
||||||
|
periods.forEach(function (period) {
|
||||||
|
var limit = group.data[period.key + '_bytes'] || 0, used = group.data[period.key + '_used_bytes'] || 0;
|
||||||
|
if (limit > 0 && (!mostUsed || used / limit > mostUsed.ratio)) mostUsed = { label: period.label, used: used, limit: limit, ratio: used / limit };
|
||||||
|
});
|
||||||
|
var text = mostUsed ? mostUsed.label + ': ' + formatSize(mostUsed.used) + ' / ' + formatSize(mostUsed.limit) : _('Unlimited');
|
||||||
|
var blocked = group.data.blocked;
|
||||||
|
return E('div', { 'class': blocked ? 'traffic-quota-exceeded' : '' },
|
||||||
|
group.label + ' · ' + text + (blocked ? ' · ' + _('Blocked') : ''));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseclass.extend({
|
||||||
|
blockedText: blockedText,
|
||||||
|
summary: summary,
|
||||||
|
save: function (quota) {
|
||||||
|
return callSave(JSON.stringify(quota)).then(function (result) {
|
||||||
|
if (!result || result.success !== true) {
|
||||||
|
var errors = {
|
||||||
|
'Update the Bandix service to configure separate upload and download quotas.': _('Update the Bandix service to configure separate upload and download quotas.'),
|
||||||
|
'Failed to connect to Bandix service': _('Failed to connect to Bandix service')
|
||||||
|
};
|
||||||
|
throw new Error(errors[result && result.error] || _('Failed to save traffic quota'));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
create: function (options) {
|
||||||
|
var inputs = {}, badges = {}, busy = false, ready = false, supported = false, error = null, current = null;
|
||||||
|
var message = E('div', { 'class': 'traffic-quota-message', role: 'status' });
|
||||||
|
var combinedState = E('span', { 'class': 'traffic-quota-status unconfigured' });
|
||||||
|
var separateOption = E('option', { value: 'directional' }, _('Separate upload / download'));
|
||||||
|
var mode = E('select', { 'class': 'cbi-input-select', id: 'traffic-quota-mode' }, [
|
||||||
|
E('option', { value: 'combined' }, _('Combined quota')), separateOption
|
||||||
|
]);
|
||||||
|
mode.value = 'combined';
|
||||||
|
var help = E('p', { 'class': 'traffic-quota-help' });
|
||||||
|
function changed() { syncDisabled(); if (options.changed) options.changed(); }
|
||||||
|
function inputGroup(direction, period) {
|
||||||
|
var id = 'traffic-quota-' + direction + '-' + period.key;
|
||||||
|
var title = (direction === 'upload' ? _('Upload') : direction === 'download' ? _('Download') : _('Combined')) + ' · ' + period.label;
|
||||||
|
var value = E('input', { type: 'number', min: '0', step: 'any', 'class': 'form-input', id: id, 'aria-label': title, placeholder: '0' });
|
||||||
|
var unit = E('select', { 'class': 'cbi-input-select', 'aria-label': title + ' · ' + _('Unit') }, units.map(function (size, index) {
|
||||||
|
return E('option', { value: String(size) }, unitNames[index]);
|
||||||
|
}));
|
||||||
|
var usage = E('div', { 'class': 'traffic-quota-period-usage' });
|
||||||
|
inputs[direction][period.key] = { value: value, unit: unit, usage: usage };
|
||||||
|
value.addEventListener('input', changed); unit.addEventListener('change', changed);
|
||||||
|
return E('div', { 'class': 'traffic-quota-period' }, [
|
||||||
|
E('label', { 'class': 'form-label', 'for': id }, period.label),
|
||||||
|
E('div', { 'class': 'traffic-quota-input-row' }, [value, unit]), usage
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
function fields(direction) {
|
||||||
|
inputs[direction] = {};
|
||||||
|
return periods.map(function (period) { return inputGroup(direction, period); });
|
||||||
|
}
|
||||||
|
function directionCard(direction, label) {
|
||||||
|
badges[direction] = E('span', { 'class': 'traffic-quota-status unconfigured' });
|
||||||
|
return E('section', { 'class': 'traffic-quota-direction traffic-quota-' + direction, 'aria-label': label }, [
|
||||||
|
E('div', { 'class': 'traffic-quota-header' }, [E('strong', {}, label), badges[direction]]),
|
||||||
|
E('div', {}, fields(direction))
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
var directions = E('div', { 'class': 'traffic-quota-directions' }, [
|
||||||
|
directionCard('upload', '↑ ' + _('Upload Quota')), directionCard('download', '↓ ' + _('Download Quota'))
|
||||||
|
]);
|
||||||
|
var combined = E('section', { 'class': 'traffic-quota-combined' }, [
|
||||||
|
E('div', { 'class': 'traffic-quota-header' }, [E('strong', {}, _('Combined quota (upload + download)')), combinedState]),
|
||||||
|
E('div', { 'class': 'traffic-quota-grid' }, fields('combined'))
|
||||||
|
]);
|
||||||
|
var root = E('div', { 'class': 'traffic-quota-form' }, [
|
||||||
|
E('div', { 'class': 'form-group' }, [E('label', { 'class': 'form-label', 'for': 'traffic-quota-mode' }, _('Quota mode')), mode]),
|
||||||
|
E('p', { 'class': 'traffic-quota-help' }, _('Only the selected mode takes effect. Switching modes preserves usage and clears the other limits when saved.')),
|
||||||
|
message, help, combined, directions
|
||||||
|
]);
|
||||||
|
function fill(input, bytes, period) {
|
||||||
|
var unit = bytes > 0 ? 1 : period === 'minute' ? 1048576 : 1073741824;
|
||||||
|
for (var i = units.length - 1; bytes > 0 && i >= 0; i--) {
|
||||||
|
if (bytes >= units[i] && bytes % units[i] === 0) { unit = units[i]; break; }
|
||||||
|
}
|
||||||
|
input.unit.value = String(unit);
|
||||||
|
input.value.value = String(bytes / unit);
|
||||||
|
}
|
||||||
|
function canEdit() { return options.writable !== false && ready && !error && !busy; }
|
||||||
|
function canSave() { return canEdit() && (mode.value === 'combined' || supported); }
|
||||||
|
function syncDisabled() {
|
||||||
|
var directional = mode.value === 'directional';
|
||||||
|
mode.disabled = !canEdit(); separateOption.disabled = !supported;
|
||||||
|
directions.style.display = directional ? '' : 'none';
|
||||||
|
combined.style.display = directional ? 'none' : '';
|
||||||
|
help.textContent = directional
|
||||||
|
? _('WAN upload and download have separate quotas. Only the exhausted direction is blocked. Enter 0 for unlimited.')
|
||||||
|
: _('WAN upload and download are counted together. Enter 0 for unlimited.');
|
||||||
|
Object.keys(inputs).forEach(function (direction) {
|
||||||
|
var active = directional ? direction !== 'combined' : direction === 'combined';
|
||||||
|
periods.forEach(function (period) {
|
||||||
|
var input = inputs[direction][period.key];
|
||||||
|
input.value.disabled = !canSave() || !active; input.unit.disabled = !canSave() || !active;
|
||||||
|
});
|
||||||
|
var badge = direction === 'combined' ? combinedState : badges[direction];
|
||||||
|
var data = current && (direction === 'combined' ? current : current[direction]);
|
||||||
|
var pending = current && mode.value !== modeOf(current);
|
||||||
|
badge.textContent = error ? _('Unavailable') : !ready ? _('Loading...') : pending ? _('Not saved')
|
||||||
|
: !data ? _('Not configured') : data.blocked ? _('Blocked') : _('Active');
|
||||||
|
badge.classList.toggle('blocked', !pending && !!(data && data.blocked));
|
||||||
|
badge.classList.toggle('unconfigured', !data || !!error || !!pending);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
mode.addEventListener('change', changed);
|
||||||
|
return {
|
||||||
|
root: root,
|
||||||
|
canSave: canSave,
|
||||||
|
setBusy: function (value) { busy = value; syncDisabled(); },
|
||||||
|
render: function (quota, state) {
|
||||||
|
ready = state.ready; supported = state.supported; error = state.error; current = quota;
|
||||||
|
if (!state.preserveInputs && !busy) mode.value = modeOf(quota);
|
||||||
|
message.textContent = error || (!ready ? _('Loading...') : !supported
|
||||||
|
? _('This service supports combined quotas only. Update Bandix to use separate quotas.')
|
||||||
|
: !quota ? _('Usage starts when a quota is first saved.') : '');
|
||||||
|
message.classList.toggle('traffic-quota-exceeded', !!error);
|
||||||
|
Object.keys(inputs).forEach(function (direction) {
|
||||||
|
var data = quota && (direction === 'combined' ? quota : quota[direction]);
|
||||||
|
periods.forEach(function (period) {
|
||||||
|
var input = inputs[direction][period.key];
|
||||||
|
var limit = data && data[period.key + '_bytes'] || 0, used = data && data[period.key + '_used_bytes'] || 0;
|
||||||
|
if (!state.preserveInputs && !busy) fill(input, limit, period.key);
|
||||||
|
input.usage.textContent = data ? _('Used') + ': ' + options.formatSize(used) + ' / ' + (limit > 0 ? options.formatSize(limit) : _('Unlimited')) : '—';
|
||||||
|
input.usage.classList.toggle('traffic-quota-exceeded', limit > 0 && used >= limit);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
syncDisabled();
|
||||||
|
},
|
||||||
|
read: function (mac) {
|
||||||
|
var result = { mac: mac, mode: mode.value, upload: {}, download: {} };
|
||||||
|
if (!canSave()) throw new Error(_('Failed to save traffic quota'));
|
||||||
|
Object.keys(inputs).forEach(function (direction) {
|
||||||
|
var target = direction === 'combined' ? result : result[direction];
|
||||||
|
var active = mode.value === 'combined' ? direction === 'combined' : direction !== 'combined';
|
||||||
|
periods.forEach(function (period) {
|
||||||
|
if (!active) { target[period.key + '_bytes'] = 0; return; }
|
||||||
|
var input = inputs[direction][period.key], value = Number(input.value.value || 0), unit = Number(input.unit.value);
|
||||||
|
var bytes = Math.round(value * unit);
|
||||||
|
if ((input.value.validity && input.value.validity.badInput) || !Number.isFinite(value) || value < 0 || units.indexOf(unit) < 0 || !Number.isSafeInteger(bytes)) {
|
||||||
|
throw new Error(_('Quota values must be non-negative and within the supported range.'));
|
||||||
|
}
|
||||||
|
target[period.key + '_bytes'] = bytes;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -129,7 +129,7 @@ function getThemeColors() {
|
|||||||
|
|
||||||
function formatSize(bytes) {
|
function formatSize(bytes) {
|
||||||
if (!bytes || bytes === 0) return '0 B';
|
if (!bytes || bytes === 0) return '0 B';
|
||||||
var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
var units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'];
|
||||||
var i = Math.floor(Math.log(bytes) / Math.log(1024));
|
var i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
return parseFloat((bytes / Math.pow(1024, i)).toFixed(2)) + ' ' + units[i];
|
return parseFloat((bytes / Math.pow(1024, i)).toFixed(2)) + ' ' + units[i];
|
||||||
}
|
}
|
||||||
@@ -148,12 +148,13 @@ function flowEndpointCell(addr, port, information) {
|
|||||||
E('span', { 'class': 'flows-addr' }, text),
|
E('span', { 'class': 'flows-addr' }, text),
|
||||||
E('span', { 'class': 'flows-port' }, port ? ':' + port : '')
|
E('span', { 'class': 'flows-port' }, port ? ':' + port : '')
|
||||||
])];
|
])];
|
||||||
if (description) children.push(E('div', {
|
if (description) {
|
||||||
'class': 'flows-geoip', title: description.details
|
var location = [E('div', {}, description.location || description.summary)];
|
||||||
}, [
|
if (description.organization) location.push(E('div', { 'class': 'flows-geoip-network' }, description.organization));
|
||||||
E('div', {}, description.location || description.summary),
|
children.push(E('div', {
|
||||||
description.organization ? E('div', { 'class': 'flows-geoip-network' }, description.organization) : null
|
'class': 'flows-geoip', title: description.details
|
||||||
]));
|
}, location));
|
||||||
|
}
|
||||||
return E('td', { 'class': 'flows-addr-cell' }, children);
|
return E('td', { 'class': 'flows-addr-cell' }, children);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
'require rpc';
|
'require rpc';
|
||||||
'require poll';
|
'require poll';
|
||||||
'require bandix.devices as devices';
|
'require bandix.devices as devices';
|
||||||
|
'require bandix.quota as quotaUi';
|
||||||
|
|
||||||
// 颜色常量定义
|
// 颜色常量定义
|
||||||
var BANDIX_COLOR_UPLOAD = '#f97316'; // 橙色 - 上传/上行
|
var BANDIX_COLOR_UPLOAD = '#f97316'; // 橙色 - 上传/上行
|
||||||
@@ -188,7 +189,7 @@ function getThemeType() {
|
|||||||
|
|
||||||
function formatSize(bytes) {
|
function formatSize(bytes) {
|
||||||
if (bytes === 0) return '0 B';
|
if (bytes === 0) return '0 B';
|
||||||
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'];
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
return parseFloat((bytes / Math.pow(1024, i)).toFixed(2)) + ' ' + units[i];
|
return parseFloat((bytes / Math.pow(1024, i)).toFixed(2)) + ' ' + units[i];
|
||||||
}
|
}
|
||||||
@@ -201,12 +202,12 @@ function formatByterate(bytes_per_sec, unit) {
|
|||||||
if (unit === 'bits') {
|
if (unit === 'bits') {
|
||||||
// 转换为比特单位
|
// 转换为比特单位
|
||||||
const bits_per_sec = bytes_per_sec * 8;
|
const bits_per_sec = bytes_per_sec * 8;
|
||||||
const units = ['bps', 'Kbps', 'Mbps', 'Gbps', 'Tbps'];
|
const units = ['bps', 'kbps', 'Mbps', 'Gbps', 'Tbps'];
|
||||||
const i = Math.floor(Math.log(bits_per_sec) / Math.log(1000));
|
const i = Math.floor(Math.log(bits_per_sec) / Math.log(1000));
|
||||||
return parseFloat((bits_per_sec / Math.pow(1000, i)).toFixed(2)) + ' ' + units[i];
|
return parseFloat((bits_per_sec / Math.pow(1000, i)).toFixed(2)) + ' ' + units[i];
|
||||||
} else {
|
} else {
|
||||||
// 默认字节单位
|
// 默认字节单位
|
||||||
const units = ['B/s', 'KB/s', 'MB/s', 'GB/s', 'TB/s'];
|
const units = ['B/s', 'KiB/s', 'MiB/s', 'GiB/s', 'TiB/s'];
|
||||||
const i = Math.floor(Math.log(bytes_per_sec) / Math.log(1024));
|
const i = Math.floor(Math.log(bytes_per_sec) / Math.log(1024));
|
||||||
return parseFloat((bytes_per_sec / Math.pow(1024, i)).toFixed(2)) + ' ' + units[i];
|
return parseFloat((bytes_per_sec / Math.pow(1024, i)).toFixed(2)) + ' ' + units[i];
|
||||||
}
|
}
|
||||||
@@ -252,17 +253,18 @@ function parseSpeed(speedStr) {
|
|||||||
if (!speedStr || speedStr === '0' || speedStr === '0 B/s' || speedStr === '0 bps') return 0;
|
if (!speedStr || speedStr === '0' || speedStr === '0 B/s' || speedStr === '0 bps') return 0;
|
||||||
|
|
||||||
// 匹配字节单位
|
// 匹配字节单位
|
||||||
const bytesMatch = speedStr.match(/^([\d.]+)\s*([KMGT]?B\/s)$/i);
|
const bytesMatch = speedStr.match(/^([\d.]+)\s*((?:[KMGT]i?)?B\/s)$/i);
|
||||||
if (bytesMatch) {
|
if (bytesMatch) {
|
||||||
const value = parseFloat(bytesMatch[1]);
|
const value = parseFloat(bytesMatch[1]);
|
||||||
const unit = bytesMatch[2].toUpperCase();
|
// Older UI strings used KB/s for the same binary value.
|
||||||
|
const unit = bytesMatch[2].toUpperCase().replace(/^([KMGT])B\//, '$1IB/');
|
||||||
|
|
||||||
const bytesMultipliers = {
|
const bytesMultipliers = {
|
||||||
'B/S': 1,
|
'B/S': 1,
|
||||||
'KB/S': 1024,
|
'KIB/S': 1024,
|
||||||
'MB/S': 1024 * 1024,
|
'MIB/S': 1024 * 1024,
|
||||||
'GB/S': 1024 * 1024 * 1024,
|
'GIB/S': 1024 * 1024 * 1024,
|
||||||
'TB/S': 1024 * 1024 * 1024 * 1024
|
'TIB/S': 1024 * 1024 * 1024 * 1024
|
||||||
};
|
};
|
||||||
|
|
||||||
return value * (bytesMultipliers[unit] || 1);
|
return value * (bytesMultipliers[unit] || 1);
|
||||||
@@ -477,13 +479,6 @@ var callGetTrafficQuotas = rpc.declare({
|
|||||||
expect: {}
|
expect: {}
|
||||||
});
|
});
|
||||||
|
|
||||||
var callSetTrafficQuota = rpc.declare({
|
|
||||||
object: 'luci.bandix',
|
|
||||||
method: 'setTrafficQuota',
|
|
||||||
params: ['mac', 'minute_bytes', 'hourly_bytes', 'daily_bytes', 'weekly_bytes', 'monthly_bytes', 'total_bytes'],
|
|
||||||
expect: {}
|
|
||||||
});
|
|
||||||
|
|
||||||
var callDeleteTrafficQuota = rpc.declare({
|
var callDeleteTrafficQuota = rpc.declare({
|
||||||
object: 'luci.bandix',
|
object: 'luci.bandix',
|
||||||
method: 'deleteTrafficQuota',
|
method: 'deleteTrafficQuota',
|
||||||
@@ -2051,6 +2046,34 @@ return view.extend({
|
|||||||
}
|
}
|
||||||
.traffic-quota-input-row .form-input { min-width: 0; flex: 1; }
|
.traffic-quota-input-row .form-input { min-width: 0; flex: 1; }
|
||||||
.traffic-quota-input-row .cbi-input-select { width: 82px; flex-shrink: 0; }
|
.traffic-quota-input-row .cbi-input-select { width: 82px; flex-shrink: 0; }
|
||||||
|
#device-settings-modal .bandix-modal { max-width: 720px; display: flex; flex-direction: column; overflow: hidden; }
|
||||||
|
#device-settings-modal .bandix-modal-body { overflow-y: auto; min-height: 0; }
|
||||||
|
#device-settings-modal .bandix-modal-header,
|
||||||
|
#device-settings-modal .bandix-modal-footer { flex-shrink: 0; }
|
||||||
|
#device-settings-modal [hidden] { display: none !important; }
|
||||||
|
.device-settings-tabs { display: flex; gap: 6px; padding: 0 20px; flex-shrink: 0; border-bottom: 1px solid ${scheme === 'dark' ? '#444' : '#ddd'}; }
|
||||||
|
.device-settings-tabs button { appearance: none; background: transparent; border: 0; border-bottom: 2px solid transparent; padding: 12px 18px; color: inherit; font: inherit; cursor: pointer; opacity: 0.65; }
|
||||||
|
.device-settings-tabs button[aria-selected="true"] { border-bottom-color: currentColor; opacity: 1; font-weight: 600; }
|
||||||
|
.device-settings-tabs button:focus-visible { outline: 2px solid currentColor; outline-offset: -3px; }
|
||||||
|
.traffic-quota-form { color-scheme: ${scheme === 'dark' ? 'dark' : 'light'}; }
|
||||||
|
.traffic-quota-directions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.traffic-quota-direction {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid ${scheme === 'dark' ? 'rgba(255,255,255,0.16)' : 'rgba(0,0,0,0.12)'};
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.traffic-quota-period { margin-top: 12px; }
|
||||||
|
.traffic-quota-period .form-label { margin-bottom: 4px; font-size: 0.8125rem; }
|
||||||
|
.traffic-quota-period-usage { font-size: 0.75rem; opacity: 0.8; margin-top: 4px; overflow-wrap: anywhere; }
|
||||||
|
.traffic-quota-exceeded { color: #ef4444; font-weight: 600; }
|
||||||
|
.traffic-quota-help, .traffic-quota-message { font-size: 0.8125rem; margin: 0 0 12px; line-height: 1.6; }
|
||||||
|
.traffic-quota-message:empty { display: none; }
|
||||||
|
.traffic-quota-combined { margin-top: 16px; }
|
||||||
.traffic-quota-usage {
|
.traffic-quota-usage {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
@@ -2093,6 +2116,7 @@ return view.extend({
|
|||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.traffic-quota-grid { grid-template-columns: 1fr; }
|
.traffic-quota-grid { grid-template-columns: 1fr; }
|
||||||
|
.traffic-quota-directions { grid-template-columns: 1fr; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 统计区域样式 */
|
/* 统计区域样式 */
|
||||||
@@ -3366,108 +3390,64 @@ return view.extend({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createQuotaInput(period, label) {
|
// One device settings dialog; tabs retain their drafts while switching.
|
||||||
return E('div', { 'class': 'form-group', 'style': 'margin-bottom: 0;' }, [
|
var quotaForm = quotaUi.create({
|
||||||
E('label', { 'class': 'form-label' }, label),
|
formatSize: formatSize, writable: !!L.hasViewPermission(),
|
||||||
E('div', { 'class': 'traffic-quota-input-row' }, [
|
changed: function () { quotaFormDirty = true; setTrafficQuotaFormDisabled(quotaFormBusy); }
|
||||||
E('input', {
|
});
|
||||||
'type': 'number',
|
var deviceSettingsModal = E('div', { 'class': 'bandix-modal-overlay', 'id': 'device-settings-modal' }, [
|
||||||
'min': '0',
|
E('div', { 'class': 'modal-content bandix-modal', role: 'dialog', 'aria-modal': 'true', 'aria-labelledby': 'device-settings-title' }, [
|
||||||
'step': '0.01',
|
|
||||||
'class': 'form-input',
|
|
||||||
'id': 'traffic-quota-' + period + '-value',
|
|
||||||
'placeholder': '0'
|
|
||||||
}),
|
|
||||||
E('select', { 'class': 'cbi-input-select', 'id': 'traffic-quota-' + period + '-unit' }, [
|
|
||||||
E('option', { 'value': '1' }, 'B'),
|
|
||||||
E('option', { 'value': '1024' }, 'KB'),
|
|
||||||
E('option', { 'value': '1048576' }, 'MB'),
|
|
||||||
E('option', { 'value': '1073741824' }, 'GB'),
|
|
||||||
E('option', { 'value': '1099511627776' }, 'TB')
|
|
||||||
])
|
|
||||||
])
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建设备设置模态框
|
|
||||||
var bandixModal = E('div', { 'class': 'bandix-modal-overlay', 'id': 'rate-limit-bandix-modal' }, [
|
|
||||||
E('div', { 'class': 'modal-content bandix-modal' }, [
|
|
||||||
E('div', { 'class': 'bandix-modal-body', }, [
|
|
||||||
E('div', { 'class': 'device-summary', 'id': 'bandix-modal-device-summary' }),
|
|
||||||
E('div', { 'class': 'form-group' }, [
|
|
||||||
E('label', { 'class': 'form-label' }, _('Hostname')),
|
|
||||||
E('div', { 'style': 'display: flex; gap: 8px; align-items: center;' }, [
|
|
||||||
E('input', { 'type': 'text', 'class': 'form-input', 'id': 'device-hostname-input', 'placeholder': _('Please enter hostname'), 'style': 'flex: 1;' }),
|
|
||||||
E('button', { 'class': 'cbi-button cbi-button-positive', 'id': 'hostname-save-btn', 'style': 'flex-shrink: 0;' }, _('Save'))
|
|
||||||
]),
|
|
||||||
E('div', { 'style': 'font-size: 0.75rem; color: #6b7280; margin-top: 4px;' }, _('Set Hostname'))
|
|
||||||
]),
|
|
||||||
// 定时限速
|
|
||||||
E('div', { 'id': 'schedule-limit-tab' }, [
|
|
||||||
// 描述和添加规则按钮
|
|
||||||
E('div', { 'style': 'display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px;' }, [
|
|
||||||
E('span', { 'style': 'font-size: 0.875rem; opacity: 0.7;' }, _('Set rate limit rules for different time periods')),
|
|
||||||
E('button', {
|
|
||||||
'type': 'button',
|
|
||||||
'class': 'cbi-button cbi-button-action',
|
|
||||||
'id': 'schedule-add-rule-btn',
|
|
||||||
'style': 'display: flex; align-items: center; gap: 4px;'
|
|
||||||
}, [
|
|
||||||
E('span', {}, '+'),
|
|
||||||
_('Add Rule')
|
|
||||||
])
|
|
||||||
]),
|
|
||||||
// 规则列表区域
|
|
||||||
E('div', { 'class': 'schedule-rules-list', 'id': 'schedule-rules-list' })
|
|
||||||
])
|
|
||||||
]),
|
|
||||||
E('div', { 'class': 'bandix-modal-footer' }, [
|
|
||||||
E('button', { 'class': 'cbi-button cbi-button-reset', 'id': 'bandix-modal-close' }, _('Close'))
|
|
||||||
])
|
|
||||||
])
|
|
||||||
]);
|
|
||||||
|
|
||||||
document.body.appendChild(bandixModal);
|
|
||||||
|
|
||||||
// 创建独立的流量配额模态框
|
|
||||||
var trafficQuotaModal = E('div', { 'class': 'bandix-modal-overlay', 'id': 'traffic-quota-bandix-modal' }, [
|
|
||||||
E('div', { 'class': 'modal-content bandix-modal' }, [
|
|
||||||
E('div', { 'class': 'bandix-modal-header' }, [
|
E('div', { 'class': 'bandix-modal-header' }, [
|
||||||
E('h3', { 'class': 'bandix-modal-title' }, _('Traffic Quota'))
|
E('h3', { 'class': 'bandix-modal-title', id: 'device-settings-title' }, _('Device Settings'))
|
||||||
]),
|
]),
|
||||||
E('div', { 'class': 'bandix-modal-body' }, [
|
E('div', { 'class': 'device-settings-tabs', role: 'tablist', 'aria-label': _('Device Settings') }, [
|
||||||
E('div', { 'class': 'device-summary', 'id': 'traffic-quota-device-summary' }),
|
E('button', { type: 'button', role: 'tab', id: 'device-settings-rate-tab', 'aria-controls': 'device-settings-rate-panel', 'aria-selected': 'true', tabindex: '0' }, _('Rate Limits')),
|
||||||
E('div', { 'class': 'traffic-quota-section', 'style': 'margin-top: 0; padding-top: 0; border-top: none;' }, [
|
E('button', { type: 'button', role: 'tab', id: 'device-settings-quota-tab', 'aria-controls': 'device-settings-quota-panel', 'aria-selected': 'false', tabindex: '-1' }, _('Traffic Quota'))
|
||||||
E('div', { 'class': 'traffic-quota-header' }, [
|
]),
|
||||||
E('div', { 'style': 'font-weight: 600;' }, _('Quota Status')),
|
E('div', { 'class': 'bandix-modal-body', id: 'device-settings-body' }, [
|
||||||
E('span', { 'class': 'traffic-quota-status', 'id': 'traffic-quota-status' }, _('Not configured'))
|
E('div', { 'class': 'device-summary', 'id': 'bandix-modal-device-summary' }),
|
||||||
|
E('div', { id: 'device-settings-rate-panel', role: 'tabpanel', 'aria-labelledby': 'device-settings-rate-tab' }, [
|
||||||
|
E('div', { 'class': 'form-group' }, [
|
||||||
|
E('label', { 'class': 'form-label' }, _('Hostname')),
|
||||||
|
E('div', { 'style': 'display: flex; gap: 8px; align-items: center;' }, [
|
||||||
|
E('input', { 'type': 'text', 'class': 'form-input', 'id': 'device-hostname-input', 'placeholder': _('Please enter hostname'), 'style': 'flex: 1;' }),
|
||||||
|
E('button', { 'class': 'cbi-button cbi-button-positive', 'id': 'hostname-save-btn', 'style': 'flex-shrink: 0;' }, _('Save'))
|
||||||
|
]),
|
||||||
|
E('div', { 'style': 'font-size: 0.75rem; color: #6b7280; margin-top: 4px;' }, _('Set Hostname'))
|
||||||
]),
|
]),
|
||||||
E('div', { 'style': 'font-size: 0.75rem; opacity: 0.7;' },
|
// 定时限速
|
||||||
_('WAN upload and download are counted together. Enter 0 for unlimited.')),
|
E('div', { 'id': 'schedule-limit-tab' }, [
|
||||||
E('div', { 'class': 'traffic-quota-grid' }, [
|
// 描述和添加规则按钮
|
||||||
createQuotaInput('minute', _('Minute Quota')),
|
E('div', { 'style': 'display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px;' }, [
|
||||||
createQuotaInput('hourly', _('Hourly Quota')),
|
E('span', { 'style': 'font-size: 0.875rem; opacity: 0.7;' }, _('Set rate limit rules for different time periods')),
|
||||||
createQuotaInput('daily', _('Daily Quota')),
|
E('button', {
|
||||||
createQuotaInput('weekly', _('Weekly Quota')),
|
'type': 'button',
|
||||||
createQuotaInput('monthly', _('Monthly Quota')),
|
'class': 'cbi-button cbi-button-action',
|
||||||
createQuotaInput('total', _('Lifetime Quota'))
|
'id': 'schedule-add-rule-btn',
|
||||||
]),
|
'style': 'display: flex; align-items: center; gap: 4px;'
|
||||||
E('div', { 'class': 'traffic-quota-usage', 'id': 'traffic-quota-usage' }, [
|
}, [
|
||||||
E('div', { 'style': 'opacity: 0.7;' }, _('No quota usage data'))
|
E('span', {}, '+'),
|
||||||
]),
|
_('Add Rule')
|
||||||
E('div', { 'style': 'display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px;' }, [
|
])
|
||||||
E('button', { 'class': 'cbi-button cbi-button-negative', 'id': 'traffic-quota-delete-btn', 'style': 'display: none;' }, _('Delete Quota')),
|
]),
|
||||||
E('button', { 'class': 'cbi-button cbi-button-positive', 'id': 'traffic-quota-save-btn' }, _('Save Quota'))
|
// 规则列表区域
|
||||||
|
E('div', { 'class': 'schedule-rules-list', 'id': 'schedule-rules-list' })
|
||||||
])
|
])
|
||||||
])
|
|
||||||
|
]),
|
||||||
|
E('div', { id: 'device-settings-quota-panel', role: 'tabpanel', 'aria-labelledby': 'device-settings-quota-tab', hidden: '' }, [quotaForm.root])
|
||||||
]),
|
]),
|
||||||
E('div', { 'class': 'bandix-modal-footer' }, [
|
E('div', { 'class': 'bandix-modal-footer', id: 'device-settings-rate-footer' }, [
|
||||||
E('button', { 'class': 'cbi-button cbi-button-reset', 'id': 'traffic-quota-modal-close' }, _('Close'))
|
E('button', { 'class': 'cbi-button cbi-button-reset', 'id': 'bandix-modal-close' }, _('Close'))
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'bandix-modal-footer', id: 'device-settings-quota-footer', hidden: '' }, [
|
||||||
|
E('button', { 'class': 'cbi-button cbi-button-negative', 'id': 'traffic-quota-delete-btn', 'style': 'display: none;' }, _('Delete Quota')),
|
||||||
|
E('button', { 'class': 'cbi-button cbi-button-reset', 'id': 'traffic-quota-modal-close' }, _('Close')),
|
||||||
|
E('button', { 'class': 'cbi-button cbi-button-positive', 'id': 'traffic-quota-save-btn' }, _('Save Quota'))
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
|
document.body.appendChild(deviceSettingsModal);
|
||||||
document.body.appendChild(trafficQuotaModal);
|
|
||||||
|
|
||||||
// 创建添加规则模态框
|
// 创建添加规则模态框
|
||||||
var addRuleModal = E('div', { 'class': 'bandix-modal-overlay', 'id': 'add-rule-bandix-modal' }, [
|
var addRuleModal = E('div', { 'class': 'bandix-modal-overlay', 'id': 'add-rule-bandix-modal' }, [
|
||||||
@@ -3558,10 +3538,10 @@ return view.extend({
|
|||||||
E('div', { 'style': 'display: flex; gap: 8px;' }, [
|
E('div', { 'style': 'display: flex; gap: 8px;' }, [
|
||||||
E('input', { 'type': 'number', 'min': '0', 'step': '0.01', 'class': 'form-input', 'id': 'whitelist_default_wan_tx', 'placeholder': '0', 'style': 'flex: 1;' }),
|
E('input', { 'type': 'number', 'min': '0', 'step': '0.01', 'class': 'form-input', 'id': 'whitelist_default_wan_tx', 'placeholder': '0', 'style': 'flex: 1;' }),
|
||||||
E('select', { 'class': 'cbi-input-select', 'id': 'whitelist_default_wan_tx_unit', 'style': 'width: 110px; flex-shrink: 0;' }, [
|
E('select', { 'class': 'cbi-input-select', 'id': 'whitelist_default_wan_tx_unit', 'style': 'width: 110px; flex-shrink: 0;' }, [
|
||||||
E('option', { 'value': '1024' }, 'KB/s'),
|
E('option', { 'value': '1024' }, 'KiB/s'),
|
||||||
E('option', { 'value': '1048576' }, 'MB/s'),
|
E('option', { 'value': '1048576' }, 'MiB/s'),
|
||||||
E('option', { 'value': '1073741824' }, 'GB/s'),
|
E('option', { 'value': '1073741824' }, 'GiB/s'),
|
||||||
E('option', { 'value': '125' }, 'Kbps'),
|
E('option', { 'value': '125' }, 'kbps'),
|
||||||
E('option', { 'value': '125000' }, 'Mbps'),
|
E('option', { 'value': '125000' }, 'Mbps'),
|
||||||
E('option', { 'value': '125000000' }, 'Gbps')
|
E('option', { 'value': '125000000' }, 'Gbps')
|
||||||
])
|
])
|
||||||
@@ -3573,10 +3553,10 @@ return view.extend({
|
|||||||
E('div', { 'style': 'display: flex; gap: 8px;' }, [
|
E('div', { 'style': 'display: flex; gap: 8px;' }, [
|
||||||
E('input', { 'type': 'number', 'min': '0', 'step': '0.01', 'class': 'form-input', 'id': 'whitelist_default_wan_rx', 'placeholder': '0', 'style': 'flex: 1;' }),
|
E('input', { 'type': 'number', 'min': '0', 'step': '0.01', 'class': 'form-input', 'id': 'whitelist_default_wan_rx', 'placeholder': '0', 'style': 'flex: 1;' }),
|
||||||
E('select', { 'class': 'cbi-input-select', 'id': 'whitelist_default_wan_rx_unit', 'style': 'width: 110px; flex-shrink: 0;' }, [
|
E('select', { 'class': 'cbi-input-select', 'id': 'whitelist_default_wan_rx_unit', 'style': 'width: 110px; flex-shrink: 0;' }, [
|
||||||
E('option', { 'value': '1024' }, 'KB/s'),
|
E('option', { 'value': '1024' }, 'KiB/s'),
|
||||||
E('option', { 'value': '1048576' }, 'MB/s'),
|
E('option', { 'value': '1048576' }, 'MiB/s'),
|
||||||
E('option', { 'value': '1073741824' }, 'GB/s'),
|
E('option', { 'value': '1073741824' }, 'GiB/s'),
|
||||||
E('option', { 'value': '125' }, 'Kbps'),
|
E('option', { 'value': '125' }, 'kbps'),
|
||||||
E('option', { 'value': '125000' }, 'Mbps'),
|
E('option', { 'value': '125000' }, 'Mbps'),
|
||||||
E('option', { 'value': '125000000' }, 'Gbps')
|
E('option', { 'value': '125000000' }, 'Gbps')
|
||||||
])
|
])
|
||||||
@@ -3970,21 +3950,21 @@ return view.extend({
|
|||||||
downloadUnitSelect.innerHTML = '';
|
downloadUnitSelect.innerHTML = '';
|
||||||
|
|
||||||
if (speedUnit === 'bits') {
|
if (speedUnit === 'bits') {
|
||||||
uploadUnitSelect.appendChild(E('option', { 'value': '125' }, 'Kbps'));
|
uploadUnitSelect.appendChild(E('option', { 'value': '125' }, 'kbps'));
|
||||||
uploadUnitSelect.appendChild(E('option', { 'value': '125000' }, 'Mbps'));
|
uploadUnitSelect.appendChild(E('option', { 'value': '125000' }, 'Mbps'));
|
||||||
uploadUnitSelect.appendChild(E('option', { 'value': '125000000' }, 'Gbps'));
|
uploadUnitSelect.appendChild(E('option', { 'value': '125000000' }, 'Gbps'));
|
||||||
|
|
||||||
downloadUnitSelect.appendChild(E('option', { 'value': '125' }, 'Kbps'));
|
downloadUnitSelect.appendChild(E('option', { 'value': '125' }, 'kbps'));
|
||||||
downloadUnitSelect.appendChild(E('option', { 'value': '125000' }, 'Mbps'));
|
downloadUnitSelect.appendChild(E('option', { 'value': '125000' }, 'Mbps'));
|
||||||
downloadUnitSelect.appendChild(E('option', { 'value': '125000000' }, 'Gbps'));
|
downloadUnitSelect.appendChild(E('option', { 'value': '125000000' }, 'Gbps'));
|
||||||
} else {
|
} else {
|
||||||
uploadUnitSelect.appendChild(E('option', { 'value': '1024' }, 'KB/s'));
|
uploadUnitSelect.appendChild(E('option', { 'value': '1024' }, 'KiB/s'));
|
||||||
uploadUnitSelect.appendChild(E('option', { 'value': '1048576' }, 'MB/s'));
|
uploadUnitSelect.appendChild(E('option', { 'value': '1048576' }, 'MiB/s'));
|
||||||
uploadUnitSelect.appendChild(E('option', { 'value': '1073741824' }, 'GB/s'));
|
uploadUnitSelect.appendChild(E('option', { 'value': '1073741824' }, 'GiB/s'));
|
||||||
|
|
||||||
downloadUnitSelect.appendChild(E('option', { 'value': '1024' }, 'KB/s'));
|
downloadUnitSelect.appendChild(E('option', { 'value': '1024' }, 'KiB/s'));
|
||||||
downloadUnitSelect.appendChild(E('option', { 'value': '1048576' }, 'MB/s'));
|
downloadUnitSelect.appendChild(E('option', { 'value': '1048576' }, 'MiB/s'));
|
||||||
downloadUnitSelect.appendChild(E('option', { 'value': '1073741824' }, 'GB/s'));
|
downloadUnitSelect.appendChild(E('option', { 'value': '1073741824' }, 'GiB/s'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (editingRule) {
|
if (editingRule) {
|
||||||
@@ -4206,65 +4186,40 @@ return view.extend({
|
|||||||
var currentDevice = null;
|
var currentDevice = null;
|
||||||
var currentQuotaDevice = null;
|
var currentQuotaDevice = null;
|
||||||
var quotaFormDirty = false;
|
var quotaFormDirty = false;
|
||||||
|
var quotaFormBusy = false;
|
||||||
var editingRule = null;
|
var editingRule = null;
|
||||||
var showRateLimitModal;
|
var scheduleRulesRequestId = 0;
|
||||||
|
|
||||||
trafficQuotaModal.querySelectorAll('.traffic-quota-input-row input, .traffic-quota-input-row select').forEach(function (input) {
|
function selectDeviceSettingsTab(tab, focus) {
|
||||||
input.addEventListener(input.tagName === 'SELECT' ? 'change' : 'input', function () {
|
document.getElementById('device-settings-body').scrollTop = 0;
|
||||||
quotaFormDirty = true;
|
['rate', 'quota'].forEach(function (name) {
|
||||||
|
var selected = name === tab;
|
||||||
|
var button = document.getElementById('device-settings-' + name + '-tab');
|
||||||
|
button.setAttribute('aria-selected', String(selected));
|
||||||
|
button.tabIndex = selected ? 0 : -1;
|
||||||
|
document.getElementById('device-settings-' + name + '-panel').hidden = !selected;
|
||||||
|
document.getElementById('device-settings-' + name + '-footer').hidden = !selected;
|
||||||
|
if (selected && focus) button.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
['rate', 'quota'].forEach(function (tab) {
|
||||||
|
var button = document.getElementById('device-settings-' + tab + '-tab');
|
||||||
|
button.addEventListener('click', function () { selectDeviceSettingsTab(tab); });
|
||||||
|
button.addEventListener('keydown', function (event) {
|
||||||
|
var next;
|
||||||
|
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') next = tab === 'rate' ? 'quota' : 'rate';
|
||||||
|
else if (event.key === 'Home') next = 'rate';
|
||||||
|
else if (event.key === 'End') next = 'quota';
|
||||||
|
if (next) { event.preventDefault(); selectDeviceSettingsTab(next, true); }
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 显示模态框
|
function showDeviceSettingsModal(device) {
|
||||||
showRateLimitModal = function (device) {
|
if (quotaFormBusy) return;
|
||||||
currentDevice = device;
|
currentDevice = device;
|
||||||
var bandixModal = document.getElementById('rate-limit-bandix-modal');
|
|
||||||
var deviceSummary = document.getElementById('bandix-modal-device-summary');
|
|
||||||
// 清空定时限速规则列表并加载
|
|
||||||
var rulesList = document.getElementById('schedule-rules-list');
|
|
||||||
if (rulesList) {
|
|
||||||
rulesList.innerHTML = '<div class="schedule-rules-empty">' +
|
|
||||||
_('No scheduled rules yet, click "Add Rule" to start setting') +
|
|
||||||
'</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载定时限速规则列表
|
|
||||||
loadScheduleRules();
|
|
||||||
|
|
||||||
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.host || '';
|
|
||||||
|
|
||||||
|
|
||||||
// 显示模态框并添加动画
|
|
||||||
bandixModal.classList.add('show');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 隐藏模态框
|
|
||||||
function hideRateLimitModal() {
|
|
||||||
var bandixModal = document.getElementById('rate-limit-bandix-modal');
|
|
||||||
bandixModal.classList.remove('show');
|
|
||||||
|
|
||||||
// 等待动画完成后清理
|
|
||||||
setTimeout(function () {
|
|
||||||
currentDevice = null;
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showTrafficQuotaModal(device) {
|
|
||||||
currentQuotaDevice = device;
|
currentQuotaDevice = device;
|
||||||
quotaFormDirty = false;
|
quotaFormDirty = false;
|
||||||
var modal = document.getElementById('traffic-quota-bandix-modal');
|
|
||||||
var deviceSummary = document.getElementById('traffic-quota-device-summary');
|
|
||||||
var modalContent = [
|
var modalContent = [
|
||||||
E('div', { 'class': 'device-summary-name' }, device.host || device.ip4),
|
E('div', { 'class': 'device-summary-name' }, device.host || device.ip4),
|
||||||
E('div', { 'class': 'device-summary-details' }, device.ip4 + ' (' + device.mac + ')')
|
E('div', { 'class': 'device-summary-details' }, device.ip4 + ' (' + device.mac + ')')
|
||||||
@@ -4273,23 +4228,29 @@ return view.extend({
|
|||||||
if (modalBadges.length) {
|
if (modalBadges.length) {
|
||||||
modalContent.push(E('div', { 'class': 'device-summary-badges device-uplink-badges-wrap' }, modalBadges));
|
modalContent.push(E('div', { 'class': 'device-summary-badges device-uplink-badges-wrap' }, modalBadges));
|
||||||
}
|
}
|
||||||
deviceSummary.innerHTML = E('div', {}, modalContent).innerHTML;
|
var summary = document.getElementById('bandix-modal-device-summary');
|
||||||
|
summary.replaceChildren(E('div', {}, modalContent));
|
||||||
|
document.getElementById('device-hostname-input').value = device.host || '';
|
||||||
|
selectDeviceSettingsTab('rate');
|
||||||
renderTrafficQuotaForm(getTrafficQuotaForDevice(device.mac), false);
|
renderTrafficQuotaForm(getTrafficQuotaForDevice(device.mac), false);
|
||||||
modal.classList.add('show');
|
loadScheduleRules();
|
||||||
loadTrafficQuotaForm();
|
loadTrafficQuotaForm();
|
||||||
|
document.getElementById('device-settings-modal').classList.add('show');
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideTrafficQuotaModal() {
|
function hideDeviceSettingsModal() {
|
||||||
document.getElementById('traffic-quota-bandix-modal').classList.remove('show');
|
document.getElementById('device-settings-modal').classList.remove('show');
|
||||||
setTimeout(function () {
|
currentDevice = null;
|
||||||
currentQuotaDevice = null;
|
currentQuotaDevice = null;
|
||||||
quotaFormDirty = false;
|
quotaFormDirty = false;
|
||||||
}, 300);
|
scheduleRulesRequestId++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载定时限速规则列表
|
// 加载定时限速规则列表
|
||||||
function loadScheduleRules() {
|
function loadScheduleRules() {
|
||||||
if (!currentDevice) return;
|
if (!currentDevice) return;
|
||||||
|
var deviceMac = currentDevice.mac;
|
||||||
|
var requestId = ++scheduleRulesRequestId;
|
||||||
|
|
||||||
var rulesList = document.getElementById('schedule-rules-list');
|
var rulesList = document.getElementById('schedule-rules-list');
|
||||||
if (!rulesList) return;
|
if (!rulesList) return;
|
||||||
@@ -4297,6 +4258,7 @@ return view.extend({
|
|||||||
rulesList.innerHTML = '<div style="text-align: center; padding: 20px; opacity: 0.6; font-size: 0.875rem;">' + _('Loading...') + '</div>';
|
rulesList.innerHTML = '<div style="text-align: center; padding: 20px; opacity: 0.6; font-size: 0.875rem;">' + _('Loading...') + '</div>';
|
||||||
|
|
||||||
callGetScheduleLimits().then(function (res) {
|
callGetScheduleLimits().then(function (res) {
|
||||||
|
if (!currentDevice || currentDevice.mac !== deviceMac || requestId !== scheduleRulesRequestId) return;
|
||||||
// 检查响应格式
|
// 检查响应格式
|
||||||
if (!res) {
|
if (!res) {
|
||||||
rulesList.innerHTML = '<div style="text-align: center; padding: 20px; opacity: 0.6; font-size: 0.875rem;">' + _('No schedule rules') + '</div>';
|
rulesList.innerHTML = '<div style="text-align: center; padding: 20px; opacity: 0.6; font-size: 0.875rem;">' + _('No schedule rules') + '</div>';
|
||||||
@@ -4326,7 +4288,7 @@ return view.extend({
|
|||||||
|
|
||||||
// 过滤出当前设备的规则
|
// 过滤出当前设备的规则
|
||||||
var deviceRules = limits.filter(function (rule) {
|
var deviceRules = limits.filter(function (rule) {
|
||||||
return rule && rule.mac === currentDevice.mac;
|
return rule && rule.mac === deviceMac;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 清空列表
|
// 清空列表
|
||||||
@@ -4404,6 +4366,7 @@ return view.extend({
|
|||||||
rulesList.appendChild(ruleItem);
|
rulesList.appendChild(ruleItem);
|
||||||
});
|
});
|
||||||
}).catch(function (error) {
|
}).catch(function (error) {
|
||||||
|
if (!currentDevice || currentDevice.mac !== deviceMac || requestId !== scheduleRulesRequestId) return;
|
||||||
console.error('Failed to load schedule rules:', error);
|
console.error('Failed to load schedule rules:', error);
|
||||||
var errorMsg = _('Failed to load schedule rules');
|
var errorMsg = _('Failed to load schedule rules');
|
||||||
if (error && error.message) {
|
if (error && error.message) {
|
||||||
@@ -4433,14 +4396,15 @@ return view.extend({
|
|||||||
saveButton.classList.add('btn-loading');
|
saveButton.classList.add('btn-loading');
|
||||||
saveButton.disabled = true;
|
saveButton.disabled = true;
|
||||||
|
|
||||||
callSetHostname(currentDevice.mac, newHostname).then(function (result) {
|
var savedDevice = currentDevice;
|
||||||
|
callSetHostname(savedDevice.mac, newHostname).then(function (result) {
|
||||||
// 恢复按钮状态
|
// 恢复按钮状态
|
||||||
saveButton.innerHTML = originalText;
|
saveButton.innerHTML = originalText;
|
||||||
saveButton.classList.remove('btn-loading');
|
saveButton.classList.remove('btn-loading');
|
||||||
saveButton.disabled = false;
|
saveButton.disabled = false;
|
||||||
|
|
||||||
// 更新当前设备信息
|
// 更新当前设备信息
|
||||||
currentDevice.host = newHostname;
|
savedDevice.host = newHostname;
|
||||||
|
|
||||||
// 刷新设备数据
|
// 刷新设备数据
|
||||||
updateDeviceData();
|
updateDeviceData();
|
||||||
@@ -4457,20 +4421,21 @@ return view.extend({
|
|||||||
document.getElementById('hostname-save-btn').addEventListener('click', saveHostname);
|
document.getElementById('hostname-save-btn').addEventListener('click', saveHostname);
|
||||||
|
|
||||||
document.getElementById('traffic-quota-save-btn').addEventListener('click', function () {
|
document.getElementById('traffic-quota-save-btn').addEventListener('click', function () {
|
||||||
if (!currentQuotaDevice) return;
|
if (!currentQuotaDevice || !quotaForm.canSave()) return;
|
||||||
var mac = currentQuotaDevice.mac;
|
var mac = currentQuotaDevice.mac;
|
||||||
var values = ['minute', 'hourly', 'daily', 'weekly', 'monthly', 'total'].map(readQuotaInput);
|
var payload;
|
||||||
if (values.some(function (value) { return value === null; })) {
|
try { payload = quotaForm.read(mac); } catch (error) {
|
||||||
ui.addNotification(null, E('p', {}, _('Quota values must be non-negative and within the supported range.')), 'error');
|
ui.addNotification(null, E('p', {}, error.message), 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var button = this;
|
var button = this;
|
||||||
button.disabled = true;
|
button.disabled = true;
|
||||||
setTrafficQuotaFormDisabled(true);
|
setTrafficQuotaFormDisabled(true);
|
||||||
callSetTrafficQuota(mac, values[0], values[1], values[2], values[3], values[4], values[5]).then(function (result) {
|
quotaUi.save(payload).then(function (result) {
|
||||||
if (result && result.success === false) throw new Error(result.error || _('Failed to save traffic quota'));
|
if (result && result.success === false) throw new Error(result.error || _('Failed to save traffic quota'));
|
||||||
return fetchAllTrafficQuotas(true);
|
return fetchAllTrafficQuotas(true);
|
||||||
}).then(function () {
|
}).then(function () {
|
||||||
|
setTrafficQuotaFormDisabled(false);
|
||||||
if (currentQuotaDevice && currentQuotaDevice.mac === mac) {
|
if (currentQuotaDevice && currentQuotaDevice.mac === mac) {
|
||||||
quotaFormDirty = false;
|
quotaFormDirty = false;
|
||||||
renderTrafficQuotaForm(getTrafficQuotaForDevice(mac), false);
|
renderTrafficQuotaForm(getTrafficQuotaForDevice(mac), false);
|
||||||
@@ -4478,23 +4443,26 @@ return view.extend({
|
|||||||
updateDeviceData();
|
updateDeviceData();
|
||||||
}).catch(function (error) {
|
}).catch(function (error) {
|
||||||
ui.addNotification(null, E('p', {}, _('Failed to save traffic quota') + ': ' + (error.message || error)), 'error');
|
ui.addNotification(null, E('p', {}, _('Failed to save traffic quota') + ': ' + (error.message || error)), 'error');
|
||||||
|
if (currentQuotaDevice) renderTrafficQuotaForm(getTrafficQuotaForDevice(currentQuotaDevice.mac), true);
|
||||||
}).finally(function () {
|
}).finally(function () {
|
||||||
button.disabled = false;
|
|
||||||
setTrafficQuotaFormDisabled(false);
|
setTrafficQuotaFormDisabled(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('traffic-quota-delete-btn').addEventListener('click', function () {
|
document.getElementById('traffic-quota-delete-btn').addEventListener('click', function () {
|
||||||
if (!currentQuotaDevice) return;
|
if (!currentQuotaDevice || quotaFormBusy || !L.hasViewPermission()) return;
|
||||||
var mac = currentQuotaDevice.mac;
|
var mac = currentQuotaDevice.mac;
|
||||||
showConfirmDialog(
|
showConfirmDialog(
|
||||||
_('Delete Traffic Quota'),
|
_('Delete Traffic Quota'),
|
||||||
_('Delete this quota and all of its accumulated usage?'),
|
_('Delete this quota and all of its accumulated usage?'),
|
||||||
function () {
|
function () {
|
||||||
|
if (quotaFormBusy) return;
|
||||||
|
setTrafficQuotaFormDisabled(true);
|
||||||
callDeleteTrafficQuota(mac).then(function (result) {
|
callDeleteTrafficQuota(mac).then(function (result) {
|
||||||
if (result && result.success === false) throw new Error(result.error || _('Failed to delete traffic quota'));
|
if (!result || result.success !== true) throw new Error(result && result.error || _('Failed to delete traffic quota'));
|
||||||
return fetchAllTrafficQuotas(true);
|
return fetchAllTrafficQuotas(true);
|
||||||
}).then(function () {
|
}).then(function () {
|
||||||
|
setTrafficQuotaFormDisabled(false);
|
||||||
if (currentQuotaDevice && currentQuotaDevice.mac === mac) {
|
if (currentQuotaDevice && currentQuotaDevice.mac === mac) {
|
||||||
quotaFormDirty = false;
|
quotaFormDirty = false;
|
||||||
renderTrafficQuotaForm(null, false);
|
renderTrafficQuotaForm(null, false);
|
||||||
@@ -4502,14 +4470,15 @@ return view.extend({
|
|||||||
updateDeviceData();
|
updateDeviceData();
|
||||||
}).catch(function (error) {
|
}).catch(function (error) {
|
||||||
ui.addNotification(null, E('p', {}, _('Failed to delete traffic quota') + ': ' + (error.message || error)), 'error');
|
ui.addNotification(null, E('p', {}, _('Failed to delete traffic quota') + ': ' + (error.message || error)), 'error');
|
||||||
});
|
if (currentQuotaDevice) renderTrafficQuotaForm(getTrafficQuotaForDevice(currentQuotaDevice.mac), true);
|
||||||
|
}).finally(function () { setTrafficQuotaFormDisabled(false); });
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 绑定关闭按钮事件
|
// 绑定关闭按钮事件
|
||||||
document.getElementById('bandix-modal-close').addEventListener('click', hideRateLimitModal);
|
document.getElementById('bandix-modal-close').addEventListener('click', hideDeviceSettingsModal);
|
||||||
document.getElementById('traffic-quota-modal-close').addEventListener('click', hideTrafficQuotaModal);
|
document.getElementById('traffic-quota-modal-close').addEventListener('click', hideDeviceSettingsModal);
|
||||||
|
|
||||||
// 历史趋势:状态与工具
|
// 历史趋势:状态与工具
|
||||||
var latestDevices = [];
|
var latestDevices = [];
|
||||||
@@ -4523,6 +4492,8 @@ return view.extend({
|
|||||||
var allTrafficQuotas = [];
|
var allTrafficQuotas = [];
|
||||||
var trafficQuotasRequest = null;
|
var trafficQuotasRequest = null;
|
||||||
var trafficQuotasLoadError = null;
|
var trafficQuotasLoadError = null;
|
||||||
|
var trafficQuotasReady = false;
|
||||||
|
var trafficQuotasDirectional = false;
|
||||||
|
|
||||||
function fetchAllTrafficQuotas(forceRefresh) {
|
function fetchAllTrafficQuotas(forceRefresh) {
|
||||||
if (trafficQuotasRequest) {
|
if (trafficQuotasRequest) {
|
||||||
@@ -4540,15 +4511,19 @@ return view.extend({
|
|||||||
quotas = res.data.quotas;
|
quotas = res.data.quotas;
|
||||||
} else if (res && Array.isArray(res.quotas)) {
|
} else if (res && Array.isArray(res.quotas)) {
|
||||||
quotas = res.quotas;
|
quotas = res.quotas;
|
||||||
|
} else {
|
||||||
|
throw new Error(_('Failed to load traffic quotas'));
|
||||||
}
|
}
|
||||||
allTrafficQuotas = quotas;
|
allTrafficQuotas = quotas;
|
||||||
|
trafficQuotasReady = true;
|
||||||
|
trafficQuotasDirectional = !!(res.data && res.data.supports_quota_modes === true);
|
||||||
trafficQuotasLoadError = null;
|
trafficQuotasLoadError = null;
|
||||||
return quotas;
|
return quotas;
|
||||||
}).catch(function (error) {
|
}).catch(function (error) {
|
||||||
console.error('Failed to fetch traffic quotas:', error);
|
console.error('Failed to fetch traffic quotas:', error);
|
||||||
allTrafficQuotas = [];
|
|
||||||
trafficQuotasLoadError = error && error.message ? error.message : _('Failed to load traffic quotas');
|
trafficQuotasLoadError = error && error.message ? error.message : _('Failed to load traffic quotas');
|
||||||
return [];
|
if (forceRefresh) throw error;
|
||||||
|
return allTrafficQuotas;
|
||||||
}).finally(function () {
|
}).finally(function () {
|
||||||
trafficQuotasRequest = null;
|
trafficQuotasRequest = null;
|
||||||
});
|
});
|
||||||
@@ -4564,77 +4539,21 @@ return view.extend({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function quotaPeriods(quota) {
|
|
||||||
return [
|
|
||||||
{ key: 'minute', label: _('Minute'), limit: quota.minute_bytes || 0, used: quota.minute_used_bytes || 0 },
|
|
||||||
{ key: 'hourly', label: _('Hourly'), limit: quota.hourly_bytes || 0, used: quota.hourly_used_bytes || 0 },
|
|
||||||
{ key: 'daily', label: _('Daily'), limit: quota.daily_bytes || 0, used: quota.daily_used_bytes || 0 },
|
|
||||||
{ key: 'weekly', label: _('Weekly'), limit: quota.weekly_bytes || 0, used: quota.weekly_used_bytes || 0 },
|
|
||||||
{ key: 'monthly', label: _('Monthly'), limit: quota.monthly_bytes || 0, used: quota.monthly_used_bytes || 0 },
|
|
||||||
{ key: 'total', label: _('Lifetime'), limit: quota.total_bytes || 0, used: quota.total_used_bytes || 0 }
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function setQuotaInput(period, bytes) {
|
|
||||||
var valueInput = document.getElementById('traffic-quota-' + period + '-value');
|
|
||||||
var unitSelect = document.getElementById('traffic-quota-' + period + '-unit');
|
|
||||||
if (!valueInput || !unitSelect) return;
|
|
||||||
var units = [1099511627776, 1073741824, 1048576, 1024, 1];
|
|
||||||
var unit = period === 'minute' ? 1048576 : 1073741824;
|
|
||||||
if (bytes > 0) {
|
|
||||||
for (var i = 0; i < units.length; i++) {
|
|
||||||
if (bytes >= units[i] && bytes % units[i] === 0) {
|
|
||||||
unit = units[i];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
unitSelect.value = String(unit);
|
|
||||||
valueInput.value = bytes > 0 ? String(bytes / unit) : '0';
|
|
||||||
}
|
|
||||||
|
|
||||||
function readQuotaInput(period) {
|
|
||||||
var value = Number(document.getElementById('traffic-quota-' + period + '-value').value || 0);
|
|
||||||
var unit = Number(document.getElementById('traffic-quota-' + period + '-unit').value || 1);
|
|
||||||
var bytes = Math.round(value * unit);
|
|
||||||
if (!Number.isFinite(bytes) || bytes < 0 || !Number.isSafeInteger(bytes)) return null;
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setTrafficQuotaFormDisabled(disabled) {
|
function setTrafficQuotaFormDisabled(disabled) {
|
||||||
trafficQuotaModal.querySelectorAll('.traffic-quota-input-row input, .traffic-quota-input-row select').forEach(function (input) {
|
quotaFormBusy = disabled;
|
||||||
input.disabled = disabled;
|
quotaForm.setBusy(disabled);
|
||||||
});
|
document.getElementById('traffic-quota-save-btn').disabled = !quotaForm.canSave();
|
||||||
|
document.getElementById('traffic-quota-delete-btn').disabled = disabled || !L.hasViewPermission();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTrafficQuotaForm(quota, preserveInputs) {
|
function renderTrafficQuotaForm(quota, preserveInputs) {
|
||||||
if (!preserveInputs) {
|
quotaForm.render(quota, {
|
||||||
var periods = ['minute', 'hourly', 'daily', 'weekly', 'monthly', 'total'];
|
ready: trafficQuotasReady, supported: trafficQuotasDirectional, error: trafficQuotasLoadError,
|
||||||
periods.forEach(function (period) {
|
preserveInputs: preserveInputs || quotaFormBusy
|
||||||
setQuotaInput(period, quota ? (quota[period + '_bytes'] || 0) : 0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var status = document.getElementById('traffic-quota-status');
|
|
||||||
var usage = document.getElementById('traffic-quota-usage');
|
|
||||||
var deleteBtn = document.getElementById('traffic-quota-delete-btn');
|
|
||||||
if (!status || !usage || !deleteBtn) return;
|
|
||||||
|
|
||||||
status.classList.toggle('blocked', !!(quota && quota.blocked));
|
|
||||||
status.classList.toggle('unconfigured', !quota);
|
|
||||||
status.textContent = !quota ? (trafficQuotasLoadError ? _('Unavailable') : _('Not configured')) : (quota.blocked ? _('Blocked') : _('Active'));
|
|
||||||
deleteBtn.style.display = quota ? '' : 'none';
|
|
||||||
usage.innerHTML = '';
|
|
||||||
if (!quota) {
|
|
||||||
usage.appendChild(E('div', { 'style': 'opacity: 0.7;' }, trafficQuotasLoadError || _('Usage starts when a quota is first saved.')));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
quotaPeriods(quota).forEach(function (period) {
|
|
||||||
usage.appendChild(E('div', { 'class': 'traffic-quota-usage-row' }, [
|
|
||||||
E('span', {}, period.label),
|
|
||||||
E('span', {}, formatSize(period.used) + ' / ' + (period.limit > 0 ? formatSize(period.limit) : _('Unlimited')))
|
|
||||||
]));
|
|
||||||
});
|
});
|
||||||
|
var deleteBtn = document.getElementById('traffic-quota-delete-btn');
|
||||||
|
deleteBtn.style.display = quota ? '' : 'none';
|
||||||
|
setTrafficQuotaFormDisabled(quotaFormBusy);
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadTrafficQuotaForm() {
|
function loadTrafficQuotaForm() {
|
||||||
@@ -4649,23 +4568,7 @@ return view.extend({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildTrafficQuotaCell(mac) {
|
function buildTrafficQuotaCell(mac) {
|
||||||
var quota = getTrafficQuotaForDevice(mac);
|
return quotaUi.summary(getTrafficQuotaForDevice(mac), formatSize);
|
||||||
if (!quota) return null;
|
|
||||||
var periods = quotaPeriods(quota).filter(function (period) { return period.limit > 0; });
|
|
||||||
var mostUsed = null;
|
|
||||||
periods.forEach(function (period) {
|
|
||||||
var ratio = period.used / period.limit;
|
|
||||||
if (!mostUsed || ratio > mostUsed.ratio) mostUsed = { period: period, ratio: ratio };
|
|
||||||
});
|
|
||||||
var text = quota.blocked ? _('Quota exceeded') : _('Quota active');
|
|
||||||
if (mostUsed) {
|
|
||||||
text = mostUsed.period.label + ': ' + formatSize(mostUsed.period.used) + ' / ' + formatSize(mostUsed.period.limit);
|
|
||||||
}
|
|
||||||
return E('div', {
|
|
||||||
'class': 'traffic-quota-cell',
|
|
||||||
'style': quota.blocked ? 'color: #ef4444; font-weight: 600;' : '',
|
|
||||||
'title': quota.blocked ? _('WAN traffic is blocked because a quota was reached.') : _('Traffic Quota')
|
|
||||||
}, text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取所有定时限速规则
|
// 获取所有定时限速规则
|
||||||
@@ -6379,7 +6282,7 @@ return view.extend({
|
|||||||
var isOnline = isDeviceOnline(device);
|
var isOnline = isDeviceOnline(device);
|
||||||
var deviceQuota = getTrafficQuotaForDevice(device.mac);
|
var deviceQuota = getTrafficQuotaForDevice(device.mac);
|
||||||
|
|
||||||
var buttonText = 'R';
|
var buttonText = _('Settings');
|
||||||
|
|
||||||
var actionButton = E('button', {
|
var actionButton = E('button', {
|
||||||
'class': 'cbi-button cbi-button-action',
|
'class': 'cbi-button cbi-button-action',
|
||||||
@@ -6387,16 +6290,7 @@ return view.extend({
|
|||||||
}, buttonText);
|
}, buttonText);
|
||||||
|
|
||||||
actionButton.addEventListener('click', function () {
|
actionButton.addEventListener('click', function () {
|
||||||
showRateLimitModal(device);
|
showDeviceSettingsModal(device);
|
||||||
});
|
|
||||||
|
|
||||||
var quotaButton = E('button', {
|
|
||||||
'class': 'cbi-button cbi-button-neutral',
|
|
||||||
'title': _('Traffic Quota')
|
|
||||||
}, 'Q');
|
|
||||||
|
|
||||||
quotaButton.addEventListener('click', function () {
|
|
||||||
showTrafficQuotaModal(device);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
var deleteButton = E('button', {
|
var deleteButton = E('button', {
|
||||||
@@ -6433,7 +6327,7 @@ return view.extend({
|
|||||||
}),
|
}),
|
||||||
device.host || '-',
|
device.host || '-',
|
||||||
deviceQuota && deviceQuota.blocked
|
deviceQuota && deviceQuota.blocked
|
||||||
? E('span', { 'class': 'traffic-quota-blocked-badge' }, _('Quota blocked'))
|
? E('span', { 'class': 'traffic-quota-blocked-badge' }, quotaUi.blockedText(deviceQuota))
|
||||||
: ''
|
: ''
|
||||||
]),
|
]),
|
||||||
E('div', { 'class': 'device-ip' }, [
|
E('div', { 'class': 'device-ip' }, [
|
||||||
@@ -6658,7 +6552,6 @@ return view.extend({
|
|||||||
// 操作
|
// 操作
|
||||||
E('td', { 'class': 'device-actions' }, [
|
E('td', { 'class': 'device-actions' }, [
|
||||||
actionButton,
|
actionButton,
|
||||||
quotaButton,
|
|
||||||
deleteButton
|
deleteButton
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
@@ -6694,20 +6587,10 @@ return view.extend({
|
|||||||
'title': _('Settings')
|
'title': _('Settings')
|
||||||
}, buttonText);
|
}, buttonText);
|
||||||
cardActionBtn.addEventListener('click', function () {
|
cardActionBtn.addEventListener('click', function () {
|
||||||
showRateLimitModal(device);
|
showDeviceSettingsModal(device);
|
||||||
});
|
});
|
||||||
return cardActionBtn;
|
return cardActionBtn;
|
||||||
})(),
|
})(),
|
||||||
(function () {
|
|
||||||
var cardQuotaBtn = E('button', {
|
|
||||||
'class': 'cbi-button cbi-button-neutral',
|
|
||||||
'title': _('Traffic Quota')
|
|
||||||
}, 'Q');
|
|
||||||
cardQuotaBtn.addEventListener('click', function () {
|
|
||||||
showTrafficQuotaModal(device);
|
|
||||||
});
|
|
||||||
return cardQuotaBtn;
|
|
||||||
})(),
|
|
||||||
(function () {
|
(function () {
|
||||||
var cardDeleteBtn = E('button', {
|
var cardDeleteBtn = E('button', {
|
||||||
'class': 'cbi-button cbi-button-reset',
|
'class': 'cbi-button cbi-button-reset',
|
||||||
@@ -7059,7 +6942,7 @@ return view.extend({
|
|||||||
return increments.map(normalizeTrafficIncrementItem).filter(function (x) { return x; });
|
return increments.map(normalizeTrafficIncrementItem).filter(function (x) { return x; });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 为了规避 ubus 1MB 响应限制,长时间范围按块分段请求
|
// 为了规避 ubus 1MiB 响应限制,长时间范围按块分段请求
|
||||||
var TRAFFIC_INCREMENT_CHUNK_MS_HOURLY = 14 * 24 * 60 * 60 * 1000; // 14 天
|
var TRAFFIC_INCREMENT_CHUNK_MS_HOURLY = 14 * 24 * 60 * 60 * 1000; // 14 天
|
||||||
var TRAFFIC_INCREMENT_CHUNK_MS_DAILY = 60 * 24 * 60 * 60 * 1000; // 60 天
|
var TRAFFIC_INCREMENT_CHUNK_MS_DAILY = 60 * 24 * 60 * 60 * 1000; // 60 天
|
||||||
|
|
||||||
|
|||||||
@@ -694,8 +694,8 @@ return view.extend({
|
|||||||
// 添加网速单位选择选项
|
// 添加网速单位选择选项
|
||||||
o = s.option(form.ListValue, 'speed_unit', _('Speed Units'),
|
o = s.option(form.ListValue, 'speed_unit', _('Speed Units'),
|
||||||
_('Select the speed display unit format'));
|
_('Select the speed display unit format'));
|
||||||
o.value('bytes', _('Bytes Units (B/s, KB/s, MB/s)'));
|
o.value('bytes', _('Bytes Units (B/s, KiB/s, MiB/s)'));
|
||||||
o.value('bits', _('Bits Units (bps, Kbps, Mbps)'));
|
o.value('bits', _('Bits Units (bps, kbps, Mbps)'));
|
||||||
o.default = 'bytes';
|
o.default = 'bytes';
|
||||||
o.rmempty = false;
|
o.rmempty = false;
|
||||||
|
|
||||||
|
|||||||
@@ -653,11 +653,11 @@ msgstr "Unidades de velocidad"
|
|||||||
msgid "Select the speed display unit format"
|
msgid "Select the speed display unit format"
|
||||||
msgstr "Seleccione el formato de unidad de visualización de velocidad"
|
msgstr "Seleccione el formato de unidad de visualización de velocidad"
|
||||||
|
|
||||||
msgid "Bytes Units (B/s, KB/s, MB/s)"
|
msgid "Bytes Units (B/s, KiB/s, MiB/s)"
|
||||||
msgstr "Unidades de bytes (B/s, KB/s, MB/s)"
|
msgstr "Unidades de bytes (B/s, KiB/s, MiB/s)"
|
||||||
|
|
||||||
msgid "Bits Units (bps, Kbps, Mbps)"
|
msgid "Bits Units (bps, kbps, Mbps)"
|
||||||
msgstr "Unidades de bits (bps, Kbps, Mbps)"
|
msgstr "Unidades de bits (bps, kbps, Mbps)"
|
||||||
|
|
||||||
msgid "Interface Theme"
|
msgid "Interface Theme"
|
||||||
msgstr "Tema de la interfaz"
|
msgstr "Tema de la interfaz"
|
||||||
@@ -692,8 +692,8 @@ msgstr "Establecer el tiempo de espera para la detección de dispositivos sin co
|
|||||||
msgid "Realtime Traffic Period"
|
msgid "Realtime Traffic Period"
|
||||||
msgstr "Período de tráfico en tiempo real"
|
msgstr "Período de tráfico en tiempo real"
|
||||||
|
|
||||||
msgid "10 minutes interval uses about 60 KB per device"
|
msgid "10 minutes interval uses about 60 KiB per device"
|
||||||
msgstr "Intervalo de 10 minutos usa aproximadamente 60 KB por dispositivo"
|
msgstr "Intervalo de 10 minutos usa aproximadamente 60 KiB por dispositivo"
|
||||||
|
|
||||||
msgid "Data Flush Interval"
|
msgid "Data Flush Interval"
|
||||||
msgstr "Intervalo de vaciado de datos"
|
msgstr "Intervalo de vaciado de datos"
|
||||||
|
|||||||
@@ -653,11 +653,11 @@ msgstr "Unités de vitesse"
|
|||||||
msgid "Select the speed display unit format"
|
msgid "Select the speed display unit format"
|
||||||
msgstr "Sélectionner le format d'unité d'affichage de la vitesse"
|
msgstr "Sélectionner le format d'unité d'affichage de la vitesse"
|
||||||
|
|
||||||
msgid "Bytes Units (B/s, KB/s, MB/s)"
|
msgid "Bytes Units (B/s, KiB/s, MiB/s)"
|
||||||
msgstr "Unités en octets (B/s, Ko/s, Mo/s)"
|
msgstr "Unités en octets (B/s, KiB/s, MiB/s)"
|
||||||
|
|
||||||
msgid "Bits Units (bps, Kbps, Mbps)"
|
msgid "Bits Units (bps, kbps, Mbps)"
|
||||||
msgstr "Unités en bits (bps, Kbps, Mbps)"
|
msgstr "Unités en bits (bps, kbps, Mbps)"
|
||||||
|
|
||||||
msgid "Interface Theme"
|
msgid "Interface Theme"
|
||||||
msgstr "Thème de l'interface"
|
msgstr "Thème de l'interface"
|
||||||
@@ -692,8 +692,8 @@ msgstr "Définir le délai de détection de déconnexion (secondes). Les apparei
|
|||||||
msgid "Realtime Traffic Period"
|
msgid "Realtime Traffic Period"
|
||||||
msgstr "Période de trafic en temps réel"
|
msgstr "Période de trafic en temps réel"
|
||||||
|
|
||||||
msgid "10 minutes interval uses about 60 KB per device"
|
msgid "10 minutes interval uses about 60 KiB per device"
|
||||||
msgstr "Intervalle de 10 minutes utilise environ 60 Ko par appareil"
|
msgstr "Intervalle de 10 minutes utilise environ 60 KiB par appareil"
|
||||||
|
|
||||||
msgid "Data Flush Interval"
|
msgid "Data Flush Interval"
|
||||||
msgstr "Intervalle de vidage des données"
|
msgstr "Intervalle de vidage des données"
|
||||||
|
|||||||
@@ -653,11 +653,11 @@ msgstr "速度単位"
|
|||||||
msgid "Select the speed display unit format"
|
msgid "Select the speed display unit format"
|
||||||
msgstr "速度表示の単位形式を選択"
|
msgstr "速度表示の単位形式を選択"
|
||||||
|
|
||||||
msgid "Bytes Units (B/s, KB/s, MB/s)"
|
msgid "Bytes Units (B/s, KiB/s, MiB/s)"
|
||||||
msgstr "バイト単位 (B/s, KB/s, MB/s)"
|
msgstr "バイト単位 (B/s, KiB/s, MiB/s)"
|
||||||
|
|
||||||
msgid "Bits Units (bps, Kbps, Mbps)"
|
msgid "Bits Units (bps, kbps, Mbps)"
|
||||||
msgstr "ビット単位 (bps, Kbps, Mbps)"
|
msgstr "ビット単位 (bps, kbps, Mbps)"
|
||||||
|
|
||||||
msgid "Interface Theme"
|
msgid "Interface Theme"
|
||||||
msgstr "インターフェーステーマ"
|
msgstr "インターフェーステーマ"
|
||||||
@@ -692,8 +692,8 @@ msgstr "デバイスのオフライン検出のタイムアウトを設定(秒
|
|||||||
msgid "Realtime Traffic Period"
|
msgid "Realtime Traffic Period"
|
||||||
msgstr "リアルタイムトラフィック周期"
|
msgstr "リアルタイムトラフィック周期"
|
||||||
|
|
||||||
msgid "10 minutes interval uses about 60 KB per device"
|
msgid "10 minutes interval uses about 60 KiB per device"
|
||||||
msgstr "10分間隔でデバイスあたり約60KBを使用"
|
msgstr "10分間隔でデバイスあたり約60 KiBを使用"
|
||||||
|
|
||||||
msgid "Data Flush Interval"
|
msgid "Data Flush Interval"
|
||||||
msgstr "データフラッシュ間隔"
|
msgstr "データフラッシュ間隔"
|
||||||
|
|||||||
@@ -653,11 +653,11 @@ msgstr "Jednostki prędkości"
|
|||||||
msgid "Select the speed display unit format"
|
msgid "Select the speed display unit format"
|
||||||
msgstr "Wybierz format jednostki wyświetlania prędkości"
|
msgstr "Wybierz format jednostki wyświetlania prędkości"
|
||||||
|
|
||||||
msgid "Bytes Units (B/s, KB/s, MB/s)"
|
msgid "Bytes Units (B/s, KiB/s, MiB/s)"
|
||||||
msgstr "Jednostki bajtów (B/s, KB/s, MB/s)"
|
msgstr "Jednostki bajtów (B/s, KiB/s, MiB/s)"
|
||||||
|
|
||||||
msgid "Bits Units (bps, Kbps, Mbps)"
|
msgid "Bits Units (bps, kbps, Mbps)"
|
||||||
msgstr "Jednostki bitów (bps, Kbps, Mbps)"
|
msgstr "Jednostki bitów (bps, kbps, Mbps)"
|
||||||
|
|
||||||
msgid "Interface Theme"
|
msgid "Interface Theme"
|
||||||
msgstr "Motyw interfejsu"
|
msgstr "Motyw interfejsu"
|
||||||
@@ -692,8 +692,8 @@ msgstr "Ustaw limit czasu wykrywania urządzeń offline (sekundy). Urządzenia n
|
|||||||
msgid "Realtime Traffic Period"
|
msgid "Realtime Traffic Period"
|
||||||
msgstr "Okres ruchu w czasie rzeczywistym"
|
msgstr "Okres ruchu w czasie rzeczywistym"
|
||||||
|
|
||||||
msgid "10 minutes interval uses about 60 KB per device"
|
msgid "10 minutes interval uses about 60 KiB per device"
|
||||||
msgstr "Interwał 10 minut używa około 60 KB na urządzenie"
|
msgstr "Interwał 10 minut używa około 60 KiB na urządzenie"
|
||||||
|
|
||||||
msgid "Data Flush Interval"
|
msgid "Data Flush Interval"
|
||||||
msgstr "Interwał zapisu danych"
|
msgstr "Interwał zapisu danych"
|
||||||
|
|||||||
@@ -653,11 +653,11 @@ msgstr "Единицы скорости"
|
|||||||
msgid "Select the speed display unit format"
|
msgid "Select the speed display unit format"
|
||||||
msgstr "Выберите формат единиц отображения скорости"
|
msgstr "Выберите формат единиц отображения скорости"
|
||||||
|
|
||||||
msgid "Bytes Units (B/s, KB/s, MB/s)"
|
msgid "Bytes Units (B/s, KiB/s, MiB/s)"
|
||||||
msgstr "Единицы в байтах (B/s, KB/s, MB/s)"
|
msgstr "Единицы в байтах (B/s, KiB/s, MiB/s)"
|
||||||
|
|
||||||
msgid "Bits Units (bps, Kbps, Mbps)"
|
msgid "Bits Units (bps, kbps, Mbps)"
|
||||||
msgstr "Единицы в битах (bps, Kbps, Mbps)"
|
msgstr "Единицы в битах (bps, kbps, Mbps)"
|
||||||
|
|
||||||
msgid "Interface Theme"
|
msgid "Interface Theme"
|
||||||
msgstr "Тема интерфейса"
|
msgstr "Тема интерфейса"
|
||||||
@@ -692,8 +692,8 @@ msgstr "Установить таймаут определения офлайн
|
|||||||
msgid "Realtime Traffic Period"
|
msgid "Realtime Traffic Period"
|
||||||
msgstr "Период трафика в реальном времени"
|
msgstr "Период трафика в реальном времени"
|
||||||
|
|
||||||
msgid "10 minutes interval uses about 60 KB per device"
|
msgid "10 minutes interval uses about 60 KiB per device"
|
||||||
msgstr "Интервал 10 минут использует около 60 КБ на устройство"
|
msgstr "Интервал 10 минут использует около 60 KiB на устройство"
|
||||||
|
|
||||||
msgid "Data Flush Interval"
|
msgid "Data Flush Interval"
|
||||||
msgstr "Интервал сброса данных"
|
msgstr "Интервал сброса данных"
|
||||||
|
|||||||
@@ -650,11 +650,11 @@ msgstr "网速单位"
|
|||||||
msgid "Select the speed display unit format"
|
msgid "Select the speed display unit format"
|
||||||
msgstr "选择网速显示的单位格式"
|
msgstr "选择网速显示的单位格式"
|
||||||
|
|
||||||
msgid "Bytes Units (B/s, KB/s, MB/s)"
|
msgid "Bytes Units (B/s, KiB/s, MiB/s)"
|
||||||
msgstr "字节单位 (B/s, KB/s, MB/s)"
|
msgstr "字节单位 (B/s, KiB/s, MiB/s)"
|
||||||
|
|
||||||
msgid "Bits Units (bps, Kbps, Mbps)"
|
msgid "Bits Units (bps, kbps, Mbps)"
|
||||||
msgstr "比特单位 (bps, Kbps, Mbps)"
|
msgstr "比特单位 (bps, kbps, Mbps)"
|
||||||
|
|
||||||
msgid "Interface Theme"
|
msgid "Interface Theme"
|
||||||
msgstr "界面主题"
|
msgstr "界面主题"
|
||||||
@@ -689,8 +689,8 @@ msgstr "设置设备离线判断的超时时间(秒)。超过此时间未活
|
|||||||
msgid "Realtime Traffic Period"
|
msgid "Realtime Traffic Period"
|
||||||
msgstr "实时流量周期"
|
msgstr "实时流量周期"
|
||||||
|
|
||||||
msgid "10 minutes interval uses about 60 KB per device"
|
msgid "10 minutes interval uses about 60 KiB per device"
|
||||||
msgstr "10分钟历史周期,每个设备占用 60KB"
|
msgstr "10分钟历史周期,每个设备占用 60 KiB"
|
||||||
|
|
||||||
msgid "Data Flush Interval"
|
msgid "Data Flush Interval"
|
||||||
msgstr "数据 flush 间隔"
|
msgstr "数据 flush 间隔"
|
||||||
@@ -1605,3 +1605,144 @@ msgstr "本页匹配"
|
|||||||
|
|
||||||
msgid "No matching connections on this page"
|
msgid "No matching connections on this page"
|
||||||
msgstr "本页没有匹配的连接"
|
msgstr "本页没有匹配的连接"
|
||||||
|
|
||||||
|
msgid "Upload"
|
||||||
|
msgstr "上传"
|
||||||
|
|
||||||
|
msgid "Download"
|
||||||
|
msgstr "下载"
|
||||||
|
|
||||||
|
msgid "Used"
|
||||||
|
msgstr "已用"
|
||||||
|
|
||||||
|
msgid "Unit"
|
||||||
|
msgstr "单位"
|
||||||
|
|
||||||
|
msgid "Combined"
|
||||||
|
msgstr "合计"
|
||||||
|
|
||||||
|
msgid "Upload Quota"
|
||||||
|
msgstr "上传配额"
|
||||||
|
|
||||||
|
msgid "Download Quota"
|
||||||
|
msgstr "下载配额"
|
||||||
|
|
||||||
|
msgid "Upload blocked"
|
||||||
|
msgstr "上传已阻断"
|
||||||
|
|
||||||
|
msgid "Download blocked"
|
||||||
|
msgstr "下载已阻断"
|
||||||
|
|
||||||
|
msgid "Upload and download blocked"
|
||||||
|
msgstr "上传、下载已阻断"
|
||||||
|
|
||||||
|
msgid "Clear combined limits"
|
||||||
|
msgstr "清除合计限制"
|
||||||
|
|
||||||
|
msgid "Combined quota (upload + download)"
|
||||||
|
msgstr "合计配额(上传 + 下载)"
|
||||||
|
|
||||||
|
msgid "A combined limit blocks both directions when reached. Set it to 0 to remove that limit."
|
||||||
|
msgstr "达到合计上限会同时阻断上传和下载;设为 0 可取消该限制。"
|
||||||
|
|
||||||
|
msgid "WAN upload and download have separate quotas. Only the exhausted direction is blocked. Enter 0 for unlimited."
|
||||||
|
msgstr "WAN 上传、下载分别计量,超额只阻断对应方向;输入 0 表示不限量。"
|
||||||
|
|
||||||
|
msgid "Update the Bandix service to configure separate upload and download quotas."
|
||||||
|
msgstr "请更新 Bandix 服务端,以设置独立的上传、下载配额。"
|
||||||
|
|
||||||
|
msgid "Add"
|
||||||
|
msgstr "添加"
|
||||||
|
|
||||||
|
msgid "Combined quota"
|
||||||
|
msgstr "合计配额"
|
||||||
|
|
||||||
|
msgid "Edit"
|
||||||
|
msgstr "编辑"
|
||||||
|
|
||||||
|
msgid "Edit Schedule Rule"
|
||||||
|
msgstr "编辑定时规则"
|
||||||
|
|
||||||
|
msgid "Failed"
|
||||||
|
msgstr "失败"
|
||||||
|
|
||||||
|
msgid "Failed to connect to Bandix service"
|
||||||
|
msgstr "无法连接 Bandix 服务"
|
||||||
|
|
||||||
|
msgid "Failed to load data"
|
||||||
|
msgstr "加载数据失败"
|
||||||
|
|
||||||
|
msgid "Failed to load whitelist"
|
||||||
|
msgstr "加载白名单失败"
|
||||||
|
|
||||||
|
msgid "Failed to save schedule rule: "
|
||||||
|
msgstr "保存定时规则失败: "
|
||||||
|
|
||||||
|
msgid "Interface"
|
||||||
|
msgstr "接口"
|
||||||
|
|
||||||
|
msgid "Invalid MAC address"
|
||||||
|
msgstr "MAC 地址无效"
|
||||||
|
|
||||||
|
msgid "Invalid value"
|
||||||
|
msgstr "输入值无效"
|
||||||
|
|
||||||
|
msgid "Multiple time slots"
|
||||||
|
msgstr "多个时间段"
|
||||||
|
|
||||||
|
msgid "No data"
|
||||||
|
msgstr "暂无数据"
|
||||||
|
|
||||||
|
msgid "Not saved"
|
||||||
|
msgstr "未保存"
|
||||||
|
|
||||||
|
msgid "Only the selected mode takes effect. Switching modes preserves usage and clears the other limits when saved."
|
||||||
|
msgstr "仅选中的模式生效。切换模式后保存,会保留已用流量并清除另一模式的上限。"
|
||||||
|
|
||||||
|
msgid "Please select both start and end dates"
|
||||||
|
msgstr "请选择开始和结束日期"
|
||||||
|
|
||||||
|
msgid "Quota mode"
|
||||||
|
msgstr "配额模式"
|
||||||
|
|
||||||
|
msgid "Router"
|
||||||
|
msgstr "路由器"
|
||||||
|
|
||||||
|
msgid "Separate upload / download"
|
||||||
|
msgstr "独立上传/下载配额"
|
||||||
|
|
||||||
|
msgid "Show All"
|
||||||
|
msgstr "显示全部"
|
||||||
|
|
||||||
|
msgid "Show Top %d"
|
||||||
|
msgstr "显示前 %d 台"
|
||||||
|
|
||||||
|
msgid "Showing all %d devices"
|
||||||
|
msgstr "显示全部 %d 台设备"
|
||||||
|
|
||||||
|
msgid "Showing top %d of %d devices"
|
||||||
|
msgstr "显示前 %d 台,共 %d 台设备"
|
||||||
|
|
||||||
|
msgid "Start date must be earlier than end date"
|
||||||
|
msgstr "开始日期必须早于结束日期"
|
||||||
|
|
||||||
|
msgid "This service supports combined quotas only. Update Bandix to use separate quotas."
|
||||||
|
msgstr "此服务端仅支持合计配额。升级 Bandix 后可使用独立配额。"
|
||||||
|
|
||||||
|
msgid "Try using filters or smaller page size"
|
||||||
|
msgstr "请尝试使用筛选或减少每页条数"
|
||||||
|
|
||||||
|
msgid "WiFi Channel"
|
||||||
|
msgstr "WiFi 信道"
|
||||||
|
|
||||||
|
msgid "days"
|
||||||
|
msgstr "天"
|
||||||
|
|
||||||
|
msgid "hours"
|
||||||
|
msgstr "小时"
|
||||||
|
|
||||||
|
msgid "seconds"
|
||||||
|
msgstr "秒"
|
||||||
|
|
||||||
|
msgid "weeks"
|
||||||
|
msgstr "周"
|
||||||
@@ -653,11 +653,11 @@ msgstr "网速單位"
|
|||||||
msgid "Select the speed display unit format"
|
msgid "Select the speed display unit format"
|
||||||
msgstr "選擇网速顯示的單位格式"
|
msgstr "選擇网速顯示的單位格式"
|
||||||
|
|
||||||
msgid "Bytes Units (B/s, KB/s, MB/s)"
|
msgid "Bytes Units (B/s, KiB/s, MiB/s)"
|
||||||
msgstr "位元組單位 (B/s, KB/s, MB/s)"
|
msgstr "位元組單位 (B/s, KiB/s, MiB/s)"
|
||||||
|
|
||||||
msgid "Bits Units (bps, Kbps, Mbps)"
|
msgid "Bits Units (bps, kbps, Mbps)"
|
||||||
msgstr "位元單位 (bps, Kbps, Mbps)"
|
msgstr "位元單位 (bps, kbps, Mbps)"
|
||||||
|
|
||||||
msgid "Interface Theme"
|
msgid "Interface Theme"
|
||||||
msgstr "界面主題"
|
msgstr "界面主題"
|
||||||
@@ -692,8 +692,8 @@ msgstr "設定設備離線判斷的超时時間(秒)。超過此時間未活
|
|||||||
msgid "Realtime Traffic Period"
|
msgid "Realtime Traffic Period"
|
||||||
msgstr "即時流量週期"
|
msgstr "即時流量週期"
|
||||||
|
|
||||||
msgid "10 minutes interval uses about 60 KB per device"
|
msgid "10 minutes interval uses about 60 KiB per device"
|
||||||
msgstr "10分鐘歷史週期,每个設備佔用 60KB"
|
msgstr "10分鐘歷史週期,每个設備佔用 60 KiB"
|
||||||
|
|
||||||
msgid "Data Flush Interval"
|
msgid "Data Flush Interval"
|
||||||
msgstr "數据 flush 間隔"
|
msgstr "數据 flush 間隔"
|
||||||
@@ -1578,3 +1578,171 @@ msgstr "本頁符合"
|
|||||||
|
|
||||||
msgid "No matching connections on this page"
|
msgid "No matching connections on this page"
|
||||||
msgstr "本頁沒有符合的連線"
|
msgstr "本頁沒有符合的連線"
|
||||||
|
|
||||||
|
msgid "Upload"
|
||||||
|
msgstr "上傳"
|
||||||
|
|
||||||
|
msgid "Download"
|
||||||
|
msgstr "下載"
|
||||||
|
|
||||||
|
msgid "Used"
|
||||||
|
msgstr "已用"
|
||||||
|
|
||||||
|
msgid "Unit"
|
||||||
|
msgstr "單位"
|
||||||
|
|
||||||
|
msgid "Combined"
|
||||||
|
msgstr "合計"
|
||||||
|
|
||||||
|
msgid "Upload Quota"
|
||||||
|
msgstr "上傳配額"
|
||||||
|
|
||||||
|
msgid "Download Quota"
|
||||||
|
msgstr "下載配額"
|
||||||
|
|
||||||
|
msgid "Upload blocked"
|
||||||
|
msgstr "上傳已阻斷"
|
||||||
|
|
||||||
|
msgid "Download blocked"
|
||||||
|
msgstr "下載已阻斷"
|
||||||
|
|
||||||
|
msgid "Upload and download blocked"
|
||||||
|
msgstr "上傳、下載已阻斷"
|
||||||
|
|
||||||
|
msgid "Clear combined limits"
|
||||||
|
msgstr "清除合計限制"
|
||||||
|
|
||||||
|
msgid "Combined quota (upload + download)"
|
||||||
|
msgstr "合計配額(上傳 + 下載)"
|
||||||
|
|
||||||
|
msgid "A combined limit blocks both directions when reached. Set it to 0 to remove that limit."
|
||||||
|
msgstr "達到合計上限會同時阻斷上傳和下載;設為 0 可取消該限制。"
|
||||||
|
|
||||||
|
msgid "WAN upload and download have separate quotas. Only the exhausted direction is blocked. Enter 0 for unlimited."
|
||||||
|
msgstr "WAN 上傳、下載分別計量,超額只阻斷對應方向;輸入 0 表示不限量。"
|
||||||
|
|
||||||
|
msgid "Update the Bandix service to configure separate upload and download quotas."
|
||||||
|
msgstr "請更新 Bandix 服務端,以設定獨立的上傳、下載配額。"
|
||||||
|
|
||||||
|
msgid "Add"
|
||||||
|
msgstr "新增"
|
||||||
|
|
||||||
|
msgid "Combined quota"
|
||||||
|
msgstr "合計配額"
|
||||||
|
|
||||||
|
msgid "Default Rate Limit"
|
||||||
|
msgstr "預設限速"
|
||||||
|
|
||||||
|
msgid "Edit"
|
||||||
|
msgstr "編輯"
|
||||||
|
|
||||||
|
msgid "Edit Schedule Rule"
|
||||||
|
msgstr "編輯排程規則"
|
||||||
|
|
||||||
|
msgid "Exempt Devices (Whitelist)"
|
||||||
|
msgstr "豁免裝置(白名單)"
|
||||||
|
|
||||||
|
msgid "Failed"
|
||||||
|
msgstr "失敗"
|
||||||
|
|
||||||
|
msgid "Failed to connect to Bandix service"
|
||||||
|
msgstr "無法連線至 Bandix 服務"
|
||||||
|
|
||||||
|
msgid "Failed to load data"
|
||||||
|
msgstr "載入資料失敗"
|
||||||
|
|
||||||
|
msgid "Failed to load whitelist"
|
||||||
|
msgstr "載入白名單失敗"
|
||||||
|
|
||||||
|
msgid "Failed to save schedule rule: "
|
||||||
|
msgstr "儲存排程規則失敗: "
|
||||||
|
|
||||||
|
msgid "Global Rate Limit"
|
||||||
|
msgstr "全域限速"
|
||||||
|
|
||||||
|
msgid "Interface"
|
||||||
|
msgstr "介面"
|
||||||
|
|
||||||
|
msgid "Invalid MAC address"
|
||||||
|
msgstr "MAC 位址無效"
|
||||||
|
|
||||||
|
msgid "Invalid value"
|
||||||
|
msgstr "輸入值無效"
|
||||||
|
|
||||||
|
msgid "Multiple time slots"
|
||||||
|
msgstr "多個時段"
|
||||||
|
|
||||||
|
msgid "No data"
|
||||||
|
msgstr "暫無資料"
|
||||||
|
|
||||||
|
msgid "Not saved"
|
||||||
|
msgstr "尚未儲存"
|
||||||
|
|
||||||
|
msgid "Only the selected mode takes effect. Switching modes preserves usage and clears the other limits when saved."
|
||||||
|
msgstr "僅選取的模式生效。切換模式後儲存,會保留已用流量並清除另一模式的上限。"
|
||||||
|
|
||||||
|
msgid "Please select both start and end dates"
|
||||||
|
msgstr "請選擇開始與結束日期"
|
||||||
|
|
||||||
|
msgid "Quota mode"
|
||||||
|
msgstr "配額模式"
|
||||||
|
|
||||||
|
msgid "Remove this device from the list?"
|
||||||
|
msgstr "要從清單中移除此裝置嗎?"
|
||||||
|
|
||||||
|
msgid "Router"
|
||||||
|
msgstr "路由器"
|
||||||
|
|
||||||
|
msgid "Separate upload / download"
|
||||||
|
msgstr "獨立上傳/下載配額"
|
||||||
|
|
||||||
|
msgid "Show All"
|
||||||
|
msgstr "顯示全部"
|
||||||
|
|
||||||
|
msgid "Show Top %d"
|
||||||
|
msgstr "顯示前 %d 台"
|
||||||
|
|
||||||
|
msgid "Showing all %d devices"
|
||||||
|
msgstr "顯示全部 %d 台裝置"
|
||||||
|
|
||||||
|
msgid "Showing top %d of %d devices"
|
||||||
|
msgstr "顯示前 %d 台,共 %d 台裝置"
|
||||||
|
|
||||||
|
msgid "Start date must be earlier than end date"
|
||||||
|
msgstr "開始日期必須早於結束日期"
|
||||||
|
|
||||||
|
msgid "This service supports combined quotas only. Update Bandix to use separate quotas."
|
||||||
|
msgstr "此服務端僅支援合計配額。升級 Bandix 後可使用獨立配額。"
|
||||||
|
|
||||||
|
msgid "Try using filters or smaller page size"
|
||||||
|
msgstr "請嘗試使用篩選或減少每頁筆數"
|
||||||
|
|
||||||
|
msgid "Update available"
|
||||||
|
msgstr "有可用更新"
|
||||||
|
|
||||||
|
msgid "Update available, click to go to settings"
|
||||||
|
msgstr "有可用更新,點擊前往設定"
|
||||||
|
|
||||||
|
msgid "When enabled, all devices will be rate limited. Devices in the list are exempt (whitelist)."
|
||||||
|
msgstr "啟用後,所有裝置均會限速,清單中的裝置除外(白名單)。"
|
||||||
|
|
||||||
|
msgid "WiFi Channel"
|
||||||
|
msgstr "WiFi 頻道"
|
||||||
|
|
||||||
|
msgid "Wired"
|
||||||
|
msgstr "有線"
|
||||||
|
|
||||||
|
msgid "Wireless"
|
||||||
|
msgstr "無線"
|
||||||
|
|
||||||
|
msgid "days"
|
||||||
|
msgstr "天"
|
||||||
|
|
||||||
|
msgid "hours"
|
||||||
|
msgstr "小時"
|
||||||
|
|
||||||
|
msgid "seconds"
|
||||||
|
msgstr "秒"
|
||||||
|
|
||||||
|
msgid "weeks"
|
||||||
|
msgstr "週"
|
||||||
@@ -191,6 +191,97 @@ set_traffic_quota() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Normalize the six limits from the currently selected JSON object. JSON strings
|
||||||
|
# carry byte values through rpcd without the 32-bit integer parameter limit.
|
||||||
|
quota_limits_json() {
|
||||||
|
local key keys type value separator=""
|
||||||
|
json_get_keys keys
|
||||||
|
for key in $keys; do
|
||||||
|
case "$key" in
|
||||||
|
minute_bytes|hourly_bytes|daily_bytes|weekly_bytes|monthly_bytes|total_bytes) ;;
|
||||||
|
mac|mode|upload|download) [ "$1" = combined ] || return 1 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
for key in minute_bytes hourly_bytes daily_bytes weekly_bytes monthly_bytes total_bytes; do
|
||||||
|
json_get_type type "$key"
|
||||||
|
value=0
|
||||||
|
if [ -n "$type" ]; then
|
||||||
|
[ "$type" = int ] || return 1
|
||||||
|
json_get_var value "$key"
|
||||||
|
fi
|
||||||
|
case "$value" in ''|*[!0-9]*) return 1 ;; esac
|
||||||
|
[ "${#value}" -le 16 ] && [ "$value" -le 9007199254740991 ] || return 1
|
||||||
|
printf '%s"%s":%s' "$separator" "$key" "$value"
|
||||||
|
separator=,
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
set_traffic_quota_config() {
|
||||||
|
local type mac mode combined upload download request_data response supported zero
|
||||||
|
json_load "{\"quota\":$1}" 2>/dev/null || { make_error "Invalid quota configuration"; return; }
|
||||||
|
json_get_type type quota
|
||||||
|
[ "$type" = object ] || { make_error "Invalid quota configuration"; return; }
|
||||||
|
json_select quota
|
||||||
|
json_get_type type mac
|
||||||
|
json_get_var mac mac
|
||||||
|
if [ "$type" != string ] || ! printf '%s\n' "$mac" | grep -Eq '^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$'; then
|
||||||
|
make_error "Invalid MAC address"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
json_get_type type mode
|
||||||
|
mode=combined
|
||||||
|
if [ -n "$type" ]; then
|
||||||
|
[ "$type" = string ] || { make_error "Invalid quota mode"; return; }
|
||||||
|
json_get_var mode mode
|
||||||
|
fi
|
||||||
|
case "$mode" in combined|directional) ;; *) make_error "Invalid quota mode"; return ;; esac
|
||||||
|
combined=$(quota_limits_json combined) || { make_error "Invalid quota limits"; return; }
|
||||||
|
zero='"minute_bytes":0,"hourly_bytes":0,"daily_bytes":0,"weekly_bytes":0,"monthly_bytes":0,"total_bytes":0'
|
||||||
|
upload="$zero"
|
||||||
|
download="$zero"
|
||||||
|
if [ "$mode" = directional ]; then
|
||||||
|
json_get_type type upload
|
||||||
|
[ "$type" = object ] || { make_error "Upload quotas are required"; return; }
|
||||||
|
json_select upload
|
||||||
|
upload=$(quota_limits_json) || { make_error "Invalid upload quota limits"; return; }
|
||||||
|
json_select ..
|
||||||
|
json_get_type type download
|
||||||
|
[ "$type" = object ] || { make_error "Download quotas are required"; return; }
|
||||||
|
json_select download
|
||||||
|
download=$(quota_limits_json) || { make_error "Invalid download quota limits"; return; }
|
||||||
|
combined="$zero"
|
||||||
|
fi
|
||||||
|
request_data="{\"mac\":\"$mac\",\"mode\":\"$mode\",$combined,\"upload\":{$upload},\"download\":{$download}}"
|
||||||
|
|
||||||
|
# Combined requests retain the legacy fields. Separate mode requires explicit
|
||||||
|
# support: old services silently ignore fields they do not understand.
|
||||||
|
if [ "$mode" = directional ]; then
|
||||||
|
response=$(curl -s --connect-timeout 2 --max-time 5 -X GET "$BANDIX_TRAFFIC_QUOTAS_API" 2>/dev/null)
|
||||||
|
if [ $? -ne 0 ] || ! json_load "$response" 2>/dev/null || ! json_select data 2>/dev/null; then
|
||||||
|
make_error "Failed to connect to Bandix service"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
json_get_type type supports_quota_modes
|
||||||
|
json_get_var supported supports_quota_modes
|
||||||
|
if [ "$type" != boolean ] || [ "$supported" != 1 ]; then
|
||||||
|
make_error "Update the Bandix service to configure separate upload and download quotas."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
response=$(curl -s --connect-timeout 3 --max-time 10 -X POST -H "Content-Type: application/json" --data-binary "$request_data" "$BANDIX_TRAFFIC_QUOTAS_API" 2>/dev/null)
|
||||||
|
if [ $? -ne 0 ] || ! json_load "$response" 2>/dev/null; then
|
||||||
|
make_error "Failed to save traffic quota"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
json_get_var type status
|
||||||
|
if [ "$type" = success ]; then
|
||||||
|
make_success "Traffic quota saved successfully"
|
||||||
|
else
|
||||||
|
make_error "Failed to save traffic quota"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
delete_traffic_quota() {
|
delete_traffic_quota() {
|
||||||
local mac="$1"
|
local mac="$1"
|
||||||
if [ -z "$mac" ]; then
|
if [ -z "$mac" ]; then
|
||||||
@@ -1531,6 +1622,13 @@ case "$1" in
|
|||||||
json_add_object "deleteTrafficQuota"
|
json_add_object "deleteTrafficQuota"
|
||||||
json_add_string "mac"
|
json_add_string "mac"
|
||||||
json_close_object
|
json_close_object
|
||||||
|
|
||||||
|
json_add_object "setTrafficQuotaConfig"
|
||||||
|
json_add_string "quota" ""
|
||||||
|
json_close_object
|
||||||
|
json_add_object "setDirectionalTrafficQuota"
|
||||||
|
json_add_string "quota"
|
||||||
|
json_close_object
|
||||||
|
|
||||||
json_dump
|
json_dump
|
||||||
json_cleanup
|
json_cleanup
|
||||||
@@ -2000,6 +2098,20 @@ case "$1" in
|
|||||||
getTrafficQuotas)
|
getTrafficQuotas)
|
||||||
get_traffic_quotas
|
get_traffic_quotas
|
||||||
;;
|
;;
|
||||||
|
setTrafficQuotaConfig|setDirectionalTrafficQuota)
|
||||||
|
quota=""
|
||||||
|
input=""
|
||||||
|
if read -t 1 -r input; then :; fi
|
||||||
|
if [ -n "$input" ]; then
|
||||||
|
if json_load "{\"args\":$input}" 2>/dev/null && json_select args 2>/dev/null; then
|
||||||
|
json_get_var quota quota
|
||||||
|
[ -n "$quota" ] || json_get_var quota 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
quota="$3"
|
||||||
|
fi
|
||||||
|
set_traffic_quota_config "$quota"
|
||||||
|
;;
|
||||||
setTrafficQuota)
|
setTrafficQuota)
|
||||||
mac=""
|
mac=""
|
||||||
minute_bytes=""
|
minute_bytes=""
|
||||||
|
|||||||
@@ -70,6 +70,8 @@
|
|||||||
"updateScheduleLimit",
|
"updateScheduleLimit",
|
||||||
"getTrafficQuotas",
|
"getTrafficQuotas",
|
||||||
"setTrafficQuota",
|
"setTrafficQuota",
|
||||||
|
"setDirectionalTrafficQuota",
|
||||||
|
"setTrafficQuotaConfig",
|
||||||
"deleteTrafficQuota",
|
"deleteTrafficQuota",
|
||||||
"getGeoipStatus",
|
"getGeoipStatus",
|
||||||
"manageGeoip"
|
"manageGeoip"
|
||||||
|
|||||||
@@ -61,6 +61,12 @@ const callLuciWirelessDevices = rpc.declare({
|
|||||||
expect: { '': {} }
|
expect: { '': {} }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const callWirelessStatus = rpc.declare({
|
||||||
|
object: 'network.wireless',
|
||||||
|
method: 'status',
|
||||||
|
expect: { '': {} }
|
||||||
|
});
|
||||||
|
|
||||||
const callLuciBoardJSON = rpc.declare({
|
const callLuciBoardJSON = rpc.declare({
|
||||||
object: 'luci-rpc',
|
object: 'luci-rpc',
|
||||||
method: 'getBoardJSON'
|
method: 'getBoardJSON'
|
||||||
@@ -1448,6 +1454,27 @@ Network = baseclass.extend(/** @lends LuCI.network.prototype */ {
|
|||||||
}, this));
|
}, this));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Return configured QCA VAP names reported by netifd, without driver probes. */
|
||||||
|
getWifiRuntimeIfnames() {
|
||||||
|
// netifd supplies VAP names without probing disabled radios or creating VAPs.
|
||||||
|
return L.resolveDefault(callWirelessStatus(), {}).then((radios) => {
|
||||||
|
const names = Object.create(null);
|
||||||
|
for (const iface of uci.sections('wireless', 'wifi-iface')) {
|
||||||
|
const radio = radios[iface.device];
|
||||||
|
if (!radio || !['qcawifi', 'qcawificfg80211'].includes(uci.get('wireless', iface.device, 'type')))
|
||||||
|
continue;
|
||||||
|
names[iface['.name']] = null;
|
||||||
|
if (!radio.up || radio.disabled || iface.disabled == '1' ||
|
||||||
|
uci.get('wireless', iface.device, 'disabled') == '1')
|
||||||
|
continue;
|
||||||
|
const live = (radio.interfaces || []).find((net) => net.section == iface['.name']);
|
||||||
|
if (live?.ifname)
|
||||||
|
names[iface['.name']] = live.ifname;
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtain configured radio devices without querying runtime state.
|
* Obtain configured radio devices without querying runtime state.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ function shellquote(s) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function has_supported_hw_offloading_target(target) {
|
function has_supported_hw_offloading_target(target) {
|
||||||
|
if (target == 'ipq40xx/generic')
|
||||||
|
return access('/sys/module/qca_ssdk') == true &&
|
||||||
|
access('/sys/module/essedma') == true;
|
||||||
|
|
||||||
return type(target) == 'string' &&
|
return type(target) == 'string' &&
|
||||||
match(target, /^(ramips\/mt7621|mediatek\/mt76..|mediatek\/filogic|airoha\/an758[0-9])$/) != null;
|
match(target, /^(ramips\/mt7621|mediatek\/mt76..|mediatek\/filogic|airoha\/an758[0-9])$/) != null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,12 @@ function mergeUniqueValues(...lists) {
|
|||||||
return merged;
|
return merged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let runtimeIfnames = Object.create(null);
|
||||||
|
|
||||||
function getQcaFallbackIfname(device, section) {
|
function getQcaFallbackIfname(device, section) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(runtimeIfnames, section))
|
||||||
|
return runtimeIfnames[section];
|
||||||
|
|
||||||
if (/^ath\d+$/.test(String(section || '')))
|
if (/^ath\d+$/.test(String(section || '')))
|
||||||
return section;
|
return section;
|
||||||
|
|
||||||
@@ -252,13 +257,14 @@ function loadIwinfoResolver(force) {
|
|||||||
if (!force && cachedIwinfoResolver != null)
|
if (!force && cachedIwinfoResolver != null)
|
||||||
return Promise.resolve(cachedIwinfoResolver);
|
return Promise.resolve(cachedIwinfoResolver);
|
||||||
|
|
||||||
const configuredDevices = getIwinfoDevicesFromConfig();
|
cachedIwinfoResolverPromise = network.getWifiRuntimeIfnames().then((names) => {
|
||||||
const deviceRequest = configuredDevices != null
|
runtimeIfnames = names;
|
||||||
? Promise.resolve(configuredDevices)
|
const configuredDevices = getIwinfoDevicesFromConfig();
|
||||||
: L.resolveDefault(callIwinfoDevices(), []);
|
return (configuredDevices != null ? Promise.resolve(configuredDevices)
|
||||||
|
: L.resolveDefault(callIwinfoDevices(), [])).then((devices) =>
|
||||||
cachedIwinfoResolverPromise = deviceRequest.then((devices) => {
|
buildIwinfoResolver(devices, configuredDevices != null));
|
||||||
cachedIwinfoResolver = buildIwinfoResolver(devices, configuredDevices != null);
|
}).then((resolver) => {
|
||||||
|
cachedIwinfoResolver = resolver;
|
||||||
cachedIwinfoResolverPromise = null;
|
cachedIwinfoResolverPromise = null;
|
||||||
return cachedIwinfoResolver;
|
return cachedIwinfoResolver;
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
@@ -444,6 +450,9 @@ function isNetworkDisplayUp(radioNet) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getRadioDisplayName(radioDev) {
|
function getRadioDisplayName(radioDev) {
|
||||||
|
const info = cachedIwinfoInfoMap?.[radioDev.getName()];
|
||||||
|
if (info?.hardware?.name)
|
||||||
|
return info.hardware.name + (info.hwmodes_text ? ' 802.11' + info.hwmodes_text : '');
|
||||||
const name = radioDev.getI18n().replace(/ Wireless Controller .+$/, '');
|
const name = radioDev.getI18n().replace(/ Wireless Controller .+$/, '');
|
||||||
const hwtype = uci.get('wireless', radioDev.getName(), 'type');
|
const hwtype = uci.get('wireless', radioDev.getName(), 'type');
|
||||||
const hwmode = uci.get('wireless', radioDev.getName(), 'hwmode') || '';
|
const hwmode = uci.get('wireless', radioDev.getName(), 'hwmode') || '';
|
||||||
@@ -608,7 +617,9 @@ function getDisplayEncryption(radioNet) {
|
|||||||
|
|
||||||
function getDisplayBSSID(radioNet) {
|
function getDisplayBSSID(radioNet) {
|
||||||
// A radio MAC may differ from the BSSID of an individual virtual AP.
|
// A radio MAC may differ from the BSSID of an individual virtual AP.
|
||||||
|
const target = cachedIwinfoResolver?.aliasMap[radioNet.getName()];
|
||||||
const candidates = [
|
const candidates = [
|
||||||
|
cachedIwinfoInfoMap?.[target]?.bssid,
|
||||||
radioNet.getActiveBSSID(),
|
radioNet.getActiveBSSID(),
|
||||||
radioNet.getBSSID(),
|
radioNet.getBSSID(),
|
||||||
uci.get('wireless', radioNet.getName(), 'macaddr')
|
uci.get('wireless', radioNet.getName(), 'macaddr')
|
||||||
@@ -1408,7 +1419,7 @@ function getDisplaySignalValue(radioNet, hwtype, is_assoc) {
|
|||||||
if (hwtype == 'mt_dbdc' && radioNet.getMode() == 'ap')
|
if (hwtype == 'mt_dbdc' && radioNet.getMode() == 'ap')
|
||||||
return is_assoc ? getDisplayTxPower(radioNet) : null;
|
return is_assoc ? getDisplayTxPower(radioNet) : null;
|
||||||
if (isQcaWifiHwtype(hwtype) && radioNet.getMode() == 'ap' && is_assoc)
|
if (isQcaWifiHwtype(hwtype) && radioNet.getMode() == 'ap' && is_assoc)
|
||||||
return getDisplayTxPower(radioNet) ?? 30;
|
return getDisplayTxPower(radioNet);
|
||||||
|
|
||||||
return radioNet.getSignal();
|
return radioNet.getSignal();
|
||||||
}
|
}
|
||||||
@@ -1748,7 +1759,9 @@ var CBIWifiFrequencyValue = form.Value.extend({
|
|||||||
const chval = +cfg_channel;
|
const chval = +cfg_channel;
|
||||||
const allow_auto = (hwtype == 'mt_dbdc' || isQcaWifiHwtype(hwtype) ||
|
const allow_auto = (hwtype == 'mt_dbdc' || isQcaWifiHwtype(hwtype) ||
|
||||||
cfg_channel == 'auto' || L.hasSystemFeature('hostapd', 'acs'));
|
cfg_channel == 'auto' || L.hasSystemFeature('hostapd', 'acs'));
|
||||||
const configOnly = isConfigWifiDeviceDisabled(device_section) ||
|
const queryDevice = isQcaWifiHwtype(hwtype)
|
||||||
|
? cachedIwinfoResolver?.aliasMap[device_section] : device_section;
|
||||||
|
const configOnly = !queryDevice || isConfigWifiDeviceDisabled(device_section) ||
|
||||||
(isQcaWifiHwtype(hwtype) && areQcaRuntimeProbesSuspended());
|
(isQcaWifiHwtype(hwtype) && areQcaRuntimeProbesSuspended());
|
||||||
const configDevice = configOnly
|
const configDevice = configOnly
|
||||||
? network.getWifiDevicesFromConfig().find((device) => device.getName() == device_section)
|
? network.getWifiDevicesFromConfig().find((device) => device.getName() == device_section)
|
||||||
@@ -1756,8 +1769,8 @@ var CBIWifiFrequencyValue = form.Value.extend({
|
|||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
configOnly ? configDevice : network.getWifiDevice(device_section),
|
configOnly ? configDevice : network.getWifiDevice(device_section),
|
||||||
configOnly ? [] : this.callFrequencyList(device_section),
|
configOnly ? [] : this.callFrequencyList(queryDevice),
|
||||||
configOnly ? {} : this.callDeviceInfo(device_section),
|
configOnly ? {} : this.callDeviceInfo(queryDevice),
|
||||||
configOnly ? {} : L.resolveDefault(this.callWirelessStatus(), {}),
|
configOnly ? {} : L.resolveDefault(this.callWirelessStatus(), {}),
|
||||||
!configOnly && isQcaWifiHwtype(hwtype) ? L.resolveDefault(fs.exec_direct('/usr/sbin/iw', [ 'dev' ]), '') : '',
|
!configOnly && isQcaWifiHwtype(hwtype) ? L.resolveDefault(fs.exec_direct('/usr/sbin/iw', [ 'dev' ]), '') : '',
|
||||||
!configOnly && isQcaWifiHwtype(hwtype) ? L.resolveDefault(fs.exec_direct('/usr/sbin/iw', [ 'phy' ]), '') : '',
|
!configOnly && isQcaWifiHwtype(hwtype) ? L.resolveDefault(fs.exec_direct('/usr/sbin/iw', [ 'phy' ]), '') : '',
|
||||||
@@ -1851,6 +1864,12 @@ var CBIWifiFrequencyValue = form.Value.extend({
|
|||||||
if (!htmode_values.length)
|
if (!htmode_values.length)
|
||||||
htmode_values = L.toArray(devinfo.htmodes);
|
htmode_values = L.toArray(devinfo.htmodes);
|
||||||
|
|
||||||
|
// Config-only WifiDevice instances default to b/g, even for a 5 GHz radio.
|
||||||
|
if (isQcaWifiHwtype(hwtype) && Array.isArray(devinfo.hwmodes))
|
||||||
|
hwmode_values = devinfo.hwmodes;
|
||||||
|
if (isQcaWifiHwtype(hwtype) && Array.isArray(devinfo.htmodes))
|
||||||
|
htmode_values = devinfo.htmodes;
|
||||||
|
|
||||||
if (!hwmode_values.length) {
|
if (!hwmode_values.length) {
|
||||||
if (/^11be/.test(hwval) || /^EHT/.test(htval))
|
if (/^11be/.test(hwval) || /^EHT/.test(htval))
|
||||||
hwmode_values = [ 'be' ];
|
hwmode_values = [ 'be' ];
|
||||||
@@ -1904,7 +1923,8 @@ var CBIWifiFrequencyValue = form.Value.extend({
|
|||||||
const qca_ht320 = (!!(htmodelist.EHT320 || /^HT320$/.test(htval)) && qca_max_width >= 320);
|
const qca_ht320 = (!!(htmodelist.EHT320 || /^HT320$/.test(htval)) && qca_max_width >= 320);
|
||||||
|
|
||||||
this.modes = [
|
this.modes = [
|
||||||
'', 'Legacy', { available: false },
|
'', hwtype == 'qcawifi' ? (hwmodelist.a ? 'A' : 'B/G') : 'Legacy',
|
||||||
|
{ available: hwtype == 'qcawifi' && (hwmodelist.a || hwmodelist.b || hwmodelist.g) },
|
||||||
'n', 'N', { available: qca_has_n },
|
'n', 'N', { available: qca_has_n },
|
||||||
'ac', 'AC', { available: qca_has_ac },
|
'ac', 'AC', { available: qca_has_ac },
|
||||||
'ax', 'AX', { available: qca_has_ax },
|
'ax', 'AX', { available: qca_has_ax },
|
||||||
@@ -1939,13 +1959,20 @@ var CBIWifiFrequencyValue = form.Value.extend({
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (hwtype == 'qcawifi')
|
||||||
|
for (let i = 0; i < this.htmodes.ac.length; i += 3)
|
||||||
|
this.htmodes.ac[i] = this.htmodes.ac[i].replace(/^HT/, 'VHT');
|
||||||
|
|
||||||
this.bands = {
|
this.bands = {
|
||||||
'': [
|
'': [
|
||||||
'2g', '2.4 GHz', { available: has_band_channels('2g') },
|
'2g', '2.4 GHz', { available: has_band_channels('2g') },
|
||||||
'5g', '5 GHz', { available: has_band_channels('5g') },
|
'5g', '5 GHz', { available: has_band_channels('5g') },
|
||||||
'6g', '6 GHz', { available: has_band_channels('6g') }
|
'6g', '6 GHz', { available: has_band_channels('6g') }
|
||||||
],
|
],
|
||||||
'n': [ '2g', '2.4 GHz', { available: has_band_channels('2g') } ],
|
'n': [
|
||||||
|
'2g', '2.4 GHz', { available: has_band_channels('2g') },
|
||||||
|
'5g', '5 GHz', { available: hwtype == 'qcawifi' && has_band_channels('5g') }
|
||||||
|
],
|
||||||
'ac': [ '5g', '5 GHz', { available: has_band_channels('5g') } ],
|
'ac': [ '5g', '5 GHz', { available: has_band_channels('5g') } ],
|
||||||
'ax': [
|
'ax': [
|
||||||
'2g', '2.4 GHz', { available: has_band_channels('2g') },
|
'2g', '2.4 GHz', { available: has_band_channels('2g') },
|
||||||
@@ -2406,10 +2433,9 @@ var CBIWifiFrequencyValue = form.Value.extend({
|
|||||||
const qcaBand = (hwtype == 'qcawificfg80211') ? getQcaBandCode(band) : null;
|
const qcaBand = (hwtype == 'qcawificfg80211') ? getQcaBandCode(band) : null;
|
||||||
|
|
||||||
if (hwtype == 'qcawifi') {
|
if (hwtype == 'qcawifi') {
|
||||||
if (mode == 'ac')
|
// QCA10 netifd uses the band in hwmode and HT/VHT in htmode.
|
||||||
hwmode = '11ac';
|
hwmode = band == '5g' ? '11a' : '11g';
|
||||||
else if (mode == 'n')
|
setValue('htmode', mode == 'ac' ? htmode.replace(/^HT/, 'VHT') : (htmode || null));
|
||||||
hwmode = '11ng';
|
|
||||||
}
|
}
|
||||||
else if (hwtype == 'qcawificfg80211') {
|
else if (hwtype == 'qcawificfg80211') {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
@@ -2469,7 +2495,9 @@ var CBIWifiTxPowerValue = form.ListValue.extend({
|
|||||||
return form.ListValue.prototype.load.apply(this, [section_id]);
|
return form.ListValue.prototype.load.apply(this, [section_id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.callTxPowerList(section_id).then(L.bind(function(pwrlist) {
|
const queryDevice = isQcaWifiHwtype(hwtype)
|
||||||
|
? cachedIwinfoResolver?.aliasMap[device_section] : section_id;
|
||||||
|
return (queryDevice ? this.callTxPowerList(queryDevice) : Promise.resolve([])).then(L.bind(function(pwrlist) {
|
||||||
this.powerval = this.wifiNetwork ? getDisplayTxPower(this.wifiNetwork) : null;
|
this.powerval = this.wifiNetwork ? getDisplayTxPower(this.wifiNetwork) : null;
|
||||||
this.poweroff = this.wifiNetwork ? this.wifiNetwork.getTXPowerOffset() : null;
|
this.poweroff = this.wifiNetwork ? this.wifiNetwork.getTXPowerOffset() : null;
|
||||||
|
|
||||||
@@ -2673,10 +2701,12 @@ return view.extend({
|
|||||||
callSystemBoard()
|
callSystemBoard()
|
||||||
]).then((data) => {
|
]).then((data) => {
|
||||||
this.boardinfo = data[4] || {};
|
this.boardinfo = data[4] || {};
|
||||||
/* MTK capability rates need iwinfo and the PHY MCS/NSS map.
|
/* Config-only radios need their runtime iwinfo before the first render,
|
||||||
* Populate them before the first render, not the next poll. */
|
* including QCA VAP names and MTK PHY capability rates. */
|
||||||
if (!this.checkAnonymousSections() &&
|
if (!this.checkAnonymousSections() &&
|
||||||
uci.sections('wireless', 'wifi-device').some(radio => radio.type == 'mt_dbdc'))
|
uci.sections('wireless', 'wifi-device').some(radio =>
|
||||||
|
radio.type == 'mt_dbdc' ||
|
||||||
|
(isQcaWifiHwtype(radio.type) && !areQcaRuntimeProbesSuspended())))
|
||||||
return loadIwinfoInfoMap(false).then(() => data);
|
return loadIwinfoInfoMap(false).then(() => data);
|
||||||
return data;
|
return data;
|
||||||
});
|
});
|
||||||
@@ -3314,6 +3344,15 @@ return view.extend({
|
|||||||
o.depends('mode', 'ap-wds');
|
o.depends('mode', 'ap-wds');
|
||||||
o.default = o.enabled;
|
o.default = o.enabled;
|
||||||
|
|
||||||
|
if (hwtype == 'qcawifi' &&
|
||||||
|
cachedIwinfoInfoMap?.[radioNet.getWifiDeviceName()]?.hwmodes?.includes('ac')) {
|
||||||
|
o = ss.taboption('advanced', form.Flag, 'vhtmubfer', _('MU-MIMO'));
|
||||||
|
o.depends('mode', 'ap');
|
||||||
|
o.depends('mode', 'ap-wds');
|
||||||
|
o.default = o.disabled;
|
||||||
|
o.rmempty = false;
|
||||||
|
}
|
||||||
|
|
||||||
o = ss.taboption('advanced', form.Flag, 'doth', '802.11h');
|
o = ss.taboption('advanced', form.Flag, 'doth', '802.11h');
|
||||||
o.depends('mode', 'ap');
|
o.depends('mode', 'ap');
|
||||||
o.depends('mode', 'ap-wds');
|
o.depends('mode', 'ap-wds');
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
LUCI_TITLE:=LuCI Status Pages
|
LUCI_TITLE:=LuCI Status Pages
|
||||||
LUCI_DEPENDS:=+luci-base +luci-app-oui +luci-mod-dashboard +libiwinfo +rpcd-mod-iwinfo +rpcd-mod-rrdns +ucode +ucode-mod-fs +ucode-mod-ubus +ucode-mod-socket
|
LUCI_DEPENDS:=+luci-base +luci-app-oui +luci-mod-dashboard +libiwinfo +rpcd-mod-iwinfo +rpcd-mod-rrdns +ucode +ucode-mod-fs +ucode-mod-ubus +ucode-mod-socket
|
||||||
|
|
||||||
PKG_RELEASE:=35
|
PKG_RELEASE:=36
|
||||||
PKG_BUILD_DEPENDS:=iwinfo
|
PKG_BUILD_DEPENDS:=iwinfo
|
||||||
PKG_LICENSE:=Apache-2.0
|
PKG_LICENSE:=Apache-2.0
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,12 @@ function pushUnique(list, value) {
|
|||||||
list.push(value);
|
list.push(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let runtimeIfnames = Object.create(null);
|
||||||
|
|
||||||
function getQcaFallbackIfname(device, section) {
|
function getQcaFallbackIfname(device, section) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(runtimeIfnames, section))
|
||||||
|
return runtimeIfnames[section];
|
||||||
|
|
||||||
if (/^ath\d+$/.test(String(section || '')))
|
if (/^ath\d+$/.test(String(section || '')))
|
||||||
return section;
|
return section;
|
||||||
|
|
||||||
@@ -162,7 +167,7 @@ function getRadioDisplayType(radio, iwinfoInfoMap) {
|
|||||||
let name = radio.getI18n().replace(/^Generic | Wireless Controller .+$/g, '');
|
let name = radio.getI18n().replace(/^Generic | Wireless Controller .+$/g, '');
|
||||||
|
|
||||||
if (hwname && hwname != 'Generic Atheros')
|
if (hwname && hwname != 'Generic Atheros')
|
||||||
name = hwname;
|
name = hwname + (info.hwmodes_text ? ' 802.11' + info.hwmodes_text : '');
|
||||||
|
|
||||||
if (name && !/^unknown$/i.test(name) && !/^802\.11unknown$/i.test(name))
|
if (name && !/^unknown$/i.test(name) && !/^802\.11unknown$/i.test(name))
|
||||||
return name;
|
return name;
|
||||||
@@ -241,9 +246,8 @@ function getDisplayEncryption(net) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getDisplayBSSID(net) {
|
function getDisplayBSSID(net) {
|
||||||
const bssid = uci.get('wireless', net.getName(), 'macaddr') ||
|
const bssid = net.getActiveBSSID() || net.getBSSID() ||
|
||||||
uci.get('wireless', net.getWifiDeviceName(), 'macaddr') ||
|
uci.get('wireless', net.getName(), 'macaddr');
|
||||||
net.getBSSID() || net.getActiveBSSID();
|
|
||||||
|
|
||||||
if (bssid && bssid != '00:00:00:00:00:00')
|
if (bssid && bssid != '00:00:00:00:00:00')
|
||||||
return String(bssid).toUpperCase();
|
return String(bssid).toUpperCase();
|
||||||
@@ -638,18 +642,21 @@ return baseclass.extend({
|
|||||||
|
|
||||||
loadIwinfoResolver(radios, networks) {
|
loadIwinfoResolver(radios, networks) {
|
||||||
// Vendor radios are queried through configured VIFs, not global probes.
|
// Vendor radios are queried through configured VIFs, not global probes.
|
||||||
const configOnly = useConfiguredWifi();
|
return network.getWifiRuntimeIfnames().then((names) => {
|
||||||
const configuredDevices = [];
|
runtimeIfnames = names;
|
||||||
|
const configOnly = useConfiguredWifi();
|
||||||
|
const configuredDevices = [];
|
||||||
|
|
||||||
for (const iface of uci.sections('wireless', 'wifi-iface')) {
|
for (const iface of uci.sections('wireless', 'wifi-iface')) {
|
||||||
if (iface.disabled == '1' || uci.get('wireless', iface.device, 'disabled') == '1')
|
if (iface.disabled == '1' || uci.get('wireless', iface.device, 'disabled') == '1')
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
pushUnique(configuredDevices, getConfiguredIfname(iface));
|
pushUnique(configuredDevices, getConfiguredIfname(iface));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (configOnly ? Promise.resolve(configuredDevices) : L.resolveDefault(callIwinfoDevices(), [])).then((devices) => {
|
return (configOnly ? Promise.resolve(configuredDevices) : L.resolveDefault(callIwinfoDevices(), [])).then((devices) => {
|
||||||
return buildIwinfoResolver(radios, networks, devices);
|
return buildIwinfoResolver(radios, networks, devices);
|
||||||
|
});
|
||||||
}).catch(() => ({
|
}).catch(() => ({
|
||||||
deviceLookup: Object.create(null),
|
deviceLookup: Object.create(null),
|
||||||
aliasMap: Object.create(null),
|
aliasMap: Object.create(null),
|
||||||
|
|||||||
+2
-2
@@ -14,8 +14,8 @@ MISE_HASH_x86_64:=3832f39c325e343f81fe3d92b2447c5d1a5eea1bc85092bb7b6c2580622264
|
|||||||
MISE_HASH_aarch64:=06186cfbfe947049b21d58575fb0ea800cc26ed1375f20f4b678cb3a9d679437
|
MISE_HASH_aarch64:=06186cfbfe947049b21d58575fb0ea800cc26ed1375f20f4b678cb3a9d679437
|
||||||
|
|
||||||
PKG_NAME:=mise
|
PKG_NAME:=mise
|
||||||
PKG_VERSION:=2026.10.19
|
PKG_VERSION:=2026.10.20
|
||||||
PKG_RELEASE:=42
|
PKG_RELEASE:=43
|
||||||
PKG_SOURCE:=$(PKG_NAME)-v$(PKG_VERSION)-linux-$(MISE_ARCH)-musl.tar.gz
|
PKG_SOURCE:=$(PKG_NAME)-v$(PKG_VERSION)-linux-$(MISE_ARCH)-musl.tar.gz
|
||||||
PKG_SOURCE_URL:=https://github.com/jdx/mise/releases/download/v$(PKG_VERSION)/
|
PKG_SOURCE_URL:=https://github.com/jdx/mise/releases/download/v$(PKG_VERSION)/
|
||||||
PKG_HASH:=skip
|
PKG_HASH:=skip
|
||||||
|
|||||||
+2
-2
@@ -10,12 +10,12 @@ include $(INCLUDE_DIR)/kernel.mk
|
|||||||
|
|
||||||
PKG_NAME:=natflow
|
PKG_NAME:=natflow
|
||||||
PKG_VERSION:=20260930
|
PKG_VERSION:=20260930
|
||||||
PKG_RELEASE:=95
|
PKG_RELEASE:=96
|
||||||
|
|
||||||
PKG_SOURCE:=$(PKG_VERSION).tar.xz
|
PKG_SOURCE:=$(PKG_VERSION).tar.xz
|
||||||
PKG_SOURCE_URL:=https://github.com/ptpt52/natflow.git
|
PKG_SOURCE_URL:=https://github.com/ptpt52/natflow.git
|
||||||
PKG_SOURCE_PROTO:=git
|
PKG_SOURCE_PROTO:=git
|
||||||
PKG_SOURCE_VERSION:=51496417511f8dc4ebef2618206db11ddedd7773
|
PKG_SOURCE_VERSION:=7c5028430db40bb6246374f3dbbb73f1e3f4360c
|
||||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
||||||
PKG_MAINTAINER:=Chen Minqiang <ptpt52@gmail.com>
|
PKG_MAINTAINER:=Chen Minqiang <ptpt52@gmail.com>
|
||||||
PKG_LICENSE:=GPL-2.0
|
PKG_LICENSE:=GPL-2.0
|
||||||
|
|||||||
+9
-9
@@ -7,26 +7,26 @@ include $(TOPDIR)/rules.mk
|
|||||||
PKG_NAME:=node
|
PKG_NAME:=node
|
||||||
|
|
||||||
ifeq ($(CONFIG_NODEJS_22),y)
|
ifeq ($(CONFIG_NODEJS_22),y)
|
||||||
PKG_VERSION:=22.23.3
|
PKG_VERSION:=22.23.4
|
||||||
PKG_RELEASE:=2
|
PKG_RELEASE:=3
|
||||||
PKG_HASH:=skip
|
PKG_HASH:=skip
|
||||||
PATCH_DIR:=./patches/v22.x
|
PATCH_DIR:=./patches/v22.x
|
||||||
NODEJS_BIN_SUM:=1084aa36196bba4c3a5e69a1ee388a6e4ff729dad09445fbcd434b28fe3c24af
|
NODEJS_BIN_SUM:=b8492284c76a54baf58d61668dd0e41ee0fc98adabf9503d30f05d08f55b4406
|
||||||
NODE_MODULE_VERSION:=127
|
NODE_MODULE_VERSION:=127
|
||||||
else
|
else
|
||||||
ifeq ($(CONFIG_NODEJS_26),y)
|
ifeq ($(CONFIG_NODEJS_26),y)
|
||||||
PKG_VERSION:=26.12.0
|
PKG_VERSION:=26.12.1
|
||||||
PKG_RELEASE:=2
|
PKG_RELEASE:=3
|
||||||
PKG_HASH:=skip
|
PKG_HASH:=skip
|
||||||
PATCH_DIR:=./patches/v26.x
|
PATCH_DIR:=./patches/v26.x
|
||||||
NODEJS_BIN_SUM:=d9f88af72ad34f7ddf8e67289829f1698f54b6254f7d4f54b7453117ff1a3d2c
|
NODEJS_BIN_SUM:=0fd1eb7770598f52a21995edf6ed35fc690400fa437c2386af329519e9143132
|
||||||
NODE_MODULE_VERSION:=147
|
NODE_MODULE_VERSION:=147
|
||||||
else
|
else
|
||||||
PKG_VERSION:=24.22.0
|
PKG_VERSION:=24.22.1
|
||||||
PKG_RELEASE:=2
|
PKG_RELEASE:=3
|
||||||
PKG_HASH:=skip
|
PKG_HASH:=skip
|
||||||
PATCH_DIR:=./patches/v24.x
|
PATCH_DIR:=./patches/v24.x
|
||||||
NODEJS_BIN_SUM:=28ea0a438de9a13bd7891fd758eee6c7819ee8af5442dfd8d45bd65e56d4228c
|
NODEJS_BIN_SUM:=eb6728e77c576a35162c122c45ccadf224d89ff00abc8dae46cc2897c6002d7c
|
||||||
NODE_MODULE_VERSION:=137
|
NODE_MODULE_VERSION:=137
|
||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|||||||
+13
-4
@@ -1,8 +1,8 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=bandix
|
PKG_NAME:=bandix
|
||||||
PKG_VERSION:=0.12.11
|
PKG_VERSION:=0.12.12
|
||||||
PKG_RELEASE:=6
|
PKG_RELEASE:=7
|
||||||
|
|
||||||
PKG_LICENSE:=Apache-2.0
|
PKG_LICENSE:=Apache-2.0
|
||||||
PKG_MAINTAINER:=timsaya
|
PKG_MAINTAINER:=timsaya
|
||||||
@@ -13,7 +13,7 @@ include $(INCLUDE_DIR)/package.mk
|
|||||||
include $(TOPDIR)/feeds/packages/lang/rust/rust-values.mk
|
include $(TOPDIR)/feeds/packages/lang/rust/rust-values.mk
|
||||||
|
|
||||||
# 二进制文件的文件名和URL
|
# 二进制文件的文件名和URL
|
||||||
RUST_BANDIX_VERSION:=0.12.11
|
RUST_BANDIX_VERSION:=0.12.12
|
||||||
RUST_BINARY_FILENAME:=bandix-$(RUST_BANDIX_VERSION)-$(RUSTC_TARGET_ARCH).tar.gz
|
RUST_BINARY_FILENAME:=bandix-$(RUST_BANDIX_VERSION)-$(RUSTC_TARGET_ARCH).tar.gz
|
||||||
|
|
||||||
|
|
||||||
@@ -65,6 +65,15 @@ else
|
|||||||
PKG_HASH:=skip
|
PKG_HASH:=skip
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
# Build an unreleased service with an SDK without changing published hashes:
|
||||||
|
# make package/bandix/compile BANDIX_LOCAL_BINARY=/absolute/path/to/bandix
|
||||||
|
BANDIX_LOCAL_BINARY?=
|
||||||
|
ifneq ($(strip $(BANDIX_LOCAL_BINARY)),)
|
||||||
|
PKG_SOURCE:=
|
||||||
|
PKG_SOURCE_URL:=
|
||||||
|
PKG_FILE_DEPENDS+=$(BANDIX_LOCAL_BINARY)
|
||||||
|
endif
|
||||||
|
|
||||||
define Package/$(PKG_NAME)
|
define Package/$(PKG_NAME)
|
||||||
SECTION:=net
|
SECTION:=net
|
||||||
CATEGORY:=Network
|
CATEGORY:=Network
|
||||||
@@ -78,7 +87,7 @@ endef
|
|||||||
|
|
||||||
define Build/Prepare
|
define Build/Prepare
|
||||||
mkdir -p $(PKG_BUILD_DIR)/bin
|
mkdir -p $(PKG_BUILD_DIR)/bin
|
||||||
gzip -dc "$(DL_DIR)/$(PKG_SOURCE)" | tar -C $(PKG_BUILD_DIR)/bin --strip-components=1 -xf -
|
$(if $(strip $(BANDIX_LOCAL_BINARY)),$(INSTALL_BIN) "$(BANDIX_LOCAL_BINARY)" $(PKG_BUILD_DIR)/bin/bandix,gzip -dc "$(DL_DIR)/$(PKG_SOURCE)" | tar -C $(PKG_BUILD_DIR)/bin --strip-components=1 -xf -)
|
||||||
$(Build/Patch)
|
$(Build/Patch)
|
||||||
endef
|
endef
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
config bandix 'general'
|
config bandix 'general'
|
||||||
option iface 'br-lan'
|
option iface 'br-lan'
|
||||||
|
option host '127.0.0.1'
|
||||||
option port '8686'
|
option port '8686'
|
||||||
option data_dir '/usr/share/bandix'
|
option data_dir '/usr/share/bandix'
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ config bandix 'traffic'
|
|||||||
|
|
||||||
config bandix 'connections'
|
config bandix 'connections'
|
||||||
option enabled '0'
|
option enabled '0'
|
||||||
|
option geoip_dir '/usr/share/bandix-geoip'
|
||||||
|
|
||||||
|
|
||||||
config bandix 'dns'
|
config bandix 'dns'
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ reload_service() {
|
|||||||
|
|
||||||
start_service() {
|
start_service() {
|
||||||
local iface
|
local iface
|
||||||
|
local host
|
||||||
local port
|
local port
|
||||||
local data_dir
|
local data_dir
|
||||||
local log_level
|
local log_level
|
||||||
@@ -33,6 +34,7 @@ start_service() {
|
|||||||
local traffic_exclude_iface_device
|
local traffic_exclude_iface_device
|
||||||
local traffic_neighbor_flush_enable
|
local traffic_neighbor_flush_enable
|
||||||
local traffic_neighbor_flush_interval
|
local traffic_neighbor_flush_interval
|
||||||
|
local geoip_dir
|
||||||
local connections_enabled
|
local connections_enabled
|
||||||
local dns_enabled
|
local dns_enabled
|
||||||
local dns_max_records
|
local dns_max_records
|
||||||
@@ -41,6 +43,8 @@ start_service() {
|
|||||||
|
|
||||||
config_load 'bandix'
|
config_load 'bandix'
|
||||||
config_get iface 'general' 'iface'
|
config_get iface 'general' 'iface'
|
||||||
|
config_get host 'general' 'host' '127.0.0.1'
|
||||||
|
[ -n "$host" ] || host='127.0.0.1'
|
||||||
config_get port 'general' 'port'
|
config_get port 'general' 'port'
|
||||||
config_get data_dir 'general' 'data_dir'
|
config_get data_dir 'general' 'data_dir'
|
||||||
config_get log_level 'general' 'log_level'
|
config_get log_level 'general' 'log_level'
|
||||||
@@ -60,6 +64,7 @@ start_service() {
|
|||||||
config_get_bool traffic_exclude_iface_device 'traffic' 'traffic_exclude_iface_device' '0'
|
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_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 traffic_neighbor_flush_interval 'traffic' 'traffic_neighbor_flush_interval' '600'
|
||||||
|
config_get geoip_dir 'connections' 'geoip_dir' '/usr/share/bandix-geoip'
|
||||||
config_get_bool connections_enabled 'connections' 'enabled'
|
config_get_bool connections_enabled 'connections' 'enabled'
|
||||||
config_get_bool dns_enabled 'dns' 'enabled'
|
config_get_bool dns_enabled 'dns' 'enabled'
|
||||||
config_get dns_max_records 'dns' 'dns_max_records'
|
config_get dns_max_records 'dns' 'dns_max_records'
|
||||||
@@ -150,8 +155,21 @@ start_service() {
|
|||||||
args="$args --dns-flush-interval $dns_flush_interval"
|
args="$args --dns-flush-interval $dns_flush_interval"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Keep compatibility with published binaries predating these options.
|
||||||
|
local bandix_help
|
||||||
|
bandix_help=$("$PROG" --help 2>/dev/null)
|
||||||
|
if [ "$host" != '127.0.0.1' ] && ! printf '%s\n' "$bandix_help" | grep -q -- '--host'; then
|
||||||
|
logger -t bandix 'The installed Bandix binary does not support --host; upgrade Bandix to change the listening address'
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
procd_open_instance bandix
|
procd_open_instance bandix
|
||||||
procd_set_param command $PROG $args
|
procd_set_param command $PROG $args
|
||||||
|
if printf '%s\n' "$bandix_help" | grep -q -- '--host'; then
|
||||||
|
procd_append_param command --host "$host"
|
||||||
|
fi
|
||||||
|
if printf '%s\n' "$bandix_help" | grep -q -- '--geoip-dir'; then
|
||||||
|
procd_append_param command --geoip-dir "$geoip_dir"
|
||||||
|
fi
|
||||||
procd_set_param respawn ${respawn_threshold:-3600} ${respawn_timeout:-60} ${respawn_retry:-3}
|
procd_set_param respawn ${respawn_threshold:-3600} ${respawn_timeout:-60} ${respawn_retry:-3}
|
||||||
procd_set_param stderr 1
|
procd_set_param stderr 1
|
||||||
procd_set_param stdout 1
|
procd_set_param stdout 1
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
|
||||||
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
|
PKG_NAME:=xradio
|
||||||
|
PKG_RELEASE:=1
|
||||||
|
|
||||||
|
PKG_LICENSE:=GPL-2.0-only
|
||||||
|
PKG_LICENSE_FILES:=
|
||||||
|
|
||||||
|
PKG_SOURCE_URL:=https://github.com/fifteenhex/xradio.git
|
||||||
|
PKG_SOURCE_PROTO:=git
|
||||||
|
PKG_SOURCE_DATE:=2025-06-01
|
||||||
|
PKG_SOURCE_VERSION:=3ce657ae533e2213f6fda2297da1598fa11d7a03
|
||||||
|
PKG_HASH:=skip
|
||||||
|
|
||||||
|
PKG_BUILD_PARALLEL:=1
|
||||||
|
|
||||||
|
include $(INCLUDE_DIR)/kernel.mk
|
||||||
|
include $(INCLUDE_DIR)/package.mk
|
||||||
|
|
||||||
|
#
|
||||||
|
# XR819 firmware download
|
||||||
|
#
|
||||||
|
|
||||||
|
define Download/xr819
|
||||||
|
FILE:=xr819.tar.bz2
|
||||||
|
URL:=https://github.com/melsem/openwrt-lede_xradio-xr819_soc-audio/raw/master/firmware/
|
||||||
|
HASH:=skip
|
||||||
|
endef
|
||||||
|
$(eval $(call Download,xr819))
|
||||||
|
|
||||||
|
#
|
||||||
|
# Kernel module
|
||||||
|
#
|
||||||
|
|
||||||
|
define KernelPackage/xradio
|
||||||
|
SUBMENU:=Wireless Drivers
|
||||||
|
TITLE:=Xradio XR819 wireless driver
|
||||||
|
DEPENDS:=@TARGET_sunxi +kmod-mac80211 +xr819-firmware
|
||||||
|
KCONFIG:= \
|
||||||
|
CONFIG_WLAN_VENDOR_XRADIO=m \
|
||||||
|
CONFIG_XRADIO_USE_EXTENSIONS=y
|
||||||
|
FILES:=$(PKG_BUILD_DIR)/xradio_wlan.ko
|
||||||
|
AUTOLOAD:=$(call AutoProbe,xradio_wlan)
|
||||||
|
endef
|
||||||
|
|
||||||
|
NOSTDINC_FLAGS = \
|
||||||
|
-I$(PKG_BUILD_DIR) \
|
||||||
|
-I$(STAGING_DIR)/usr/include/mac80211-backport/uapi \
|
||||||
|
-I$(STAGING_DIR)/usr/include/mac80211-backport \
|
||||||
|
-I$(STAGING_DIR)/usr/include/mac80211/uapi \
|
||||||
|
-I$(STAGING_DIR)/usr/include/mac80211 \
|
||||||
|
-include backport/autoconf.h \
|
||||||
|
-include backport/backport.h
|
||||||
|
|
||||||
|
define Build/Compile
|
||||||
|
+$(MAKE) $(PKG_JOBS) -C "$(LINUX_DIR)" \
|
||||||
|
$(KERNEL_MAKE_FLAGS) \
|
||||||
|
$(PKG_MAKE_FLAGS) \
|
||||||
|
M="$(PKG_BUILD_DIR)" \
|
||||||
|
NOSTDINC_FLAGS="$(NOSTDINC_FLAGS)" \
|
||||||
|
modules
|
||||||
|
endef
|
||||||
|
|
||||||
|
$(eval $(call KernelPackage,xradio))
|
||||||
|
|
||||||
|
#
|
||||||
|
# XR819 firmware package
|
||||||
|
#
|
||||||
|
|
||||||
|
define Package/xr819-firmware
|
||||||
|
SECTION:=firmware
|
||||||
|
CATEGORY:=Firmware
|
||||||
|
TITLE:=Xradio XR819 firmware
|
||||||
|
DEPENDS:=@TARGET_sunxi
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/xr819-firmware/description
|
||||||
|
Firmware files for the Xradio XR819 wireless chipset.
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/xr819-firmware/install
|
||||||
|
$(INSTALL_DIR) $(1)/lib/firmware
|
||||||
|
$(TAR) -C $(1)/lib/firmware -xjf $(DL_DIR)/xr819.tar.bz2
|
||||||
|
endef
|
||||||
|
|
||||||
|
$(eval $(call BuildPackage,xr819-firmware))
|
||||||
|
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
From 301c85463b59270a884ce4ecc8a69bc5b70d378c Mon Sep 17 00:00:00 2001
|
||||||
|
From: koshev-msk <kosnt.shell@132gmail.com>
|
||||||
|
Date: Mon, 26 Oct 2026 10:12:34 +0300
|
||||||
|
Subject: [PATCH 1/2] Fix build with recent mac80211 and -Werror
|
||||||
|
|
||||||
|
- add radio_idx to ieee80211_ops .config and .set_rts_threshold
|
||||||
|
- add prototypes for non-static functions in main.c (-Wmissing-prototypes)
|
||||||
|
- make debug printk stubs proper statements (-Wempty-body)
|
||||||
|
---
|
||||||
|
debug.h | 14 +++++++-------
|
||||||
|
main.c | 1 +
|
||||||
|
main.h | 4 ++++
|
||||||
|
sta.c | 4 ++--
|
||||||
|
sta.h | 4 ++--
|
||||||
|
5 files changed, 16 insertions(+), 11 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/debug.h b/debug.h
|
||||||
|
index 30a59f3..90fc3b3 100644
|
||||||
|
--- a/debug.h
|
||||||
|
+++ b/debug.h
|
||||||
|
@@ -11,12 +11,12 @@
|
||||||
|
#ifndef XRADIO_DEBUG_H_INCLUDED
|
||||||
|
#define XRADIO_DEBUG_H_INCLUDED
|
||||||
|
|
||||||
|
-#define xradio_dbg(level, ...)
|
||||||
|
-#define txrx_printk(level, ...)
|
||||||
|
-#define wsm_printk(level, ...)
|
||||||
|
-#define sta_printk(level, ...)
|
||||||
|
-#define scan_printk(level, ...)
|
||||||
|
-#define ap_printk(level, ...)
|
||||||
|
-#define pm_printk(level, ...)
|
||||||
|
+#define xradio_dbg(level, ...) do { } while (0)
|
||||||
|
+#define txrx_printk(level, ...) do { } while (0)
|
||||||
|
+#define wsm_printk(level, ...) do { } while (0)
|
||||||
|
+#define sta_printk(level, ...) do { } while (0)
|
||||||
|
+#define scan_printk(level, ...) do { } while (0)
|
||||||
|
+#define ap_printk(level, ...) do { } while (0)
|
||||||
|
+#define pm_printk(level, ...) do { } while (0)
|
||||||
|
|
||||||
|
#endif /* XRADIO_DEBUG_H_INCLUDED */
|
||||||
|
diff --git a/main.c b/main.c
|
||||||
|
index a30c604..a291b3d 100644
|
||||||
|
--- a/main.c
|
||||||
|
+++ b/main.c
|
||||||
|
@@ -25,6 +25,7 @@
|
||||||
|
#include "scan.h"
|
||||||
|
#include "pm.h"
|
||||||
|
#include "sdio.h"
|
||||||
|
+#include "main.h"
|
||||||
|
|
||||||
|
/* TODO: use rates and channels from the device */
|
||||||
|
#define RATETAB_ENT(_rate, _rateid, _flags) \
|
||||||
|
diff --git a/main.h b/main.h
|
||||||
|
index 2238d75..be634a4 100644
|
||||||
|
--- a/main.h
|
||||||
|
+++ b/main.h
|
||||||
|
@@ -1,6 +1,10 @@
|
||||||
|
#ifndef __XRADIO_MAIN_H
|
||||||
|
#define __XRADIO_MAIN_H
|
||||||
|
|
||||||
|
+struct ieee80211_hw *xradio_init_common(size_t hw_priv_data_len);
|
||||||
|
+void xradio_free_common(struct ieee80211_hw *dev);
|
||||||
|
+int xradio_register_common(struct ieee80211_hw *dev);
|
||||||
|
+void xradio_unregister_common(struct ieee80211_hw *dev);
|
||||||
|
int xradio_core_init(struct sdio_func* func);
|
||||||
|
void xradio_core_deinit(struct sdio_func* func);
|
||||||
|
|
||||||
|
diff --git a/sta.c b/sta.c
|
||||||
|
index 2dddb26..db38cfb 100644
|
||||||
|
--- a/sta.c
|
||||||
|
+++ b/sta.c
|
||||||
|
@@ -381,7 +381,7 @@ int xradio_change_interface(struct ieee80211_hw *dev,
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
-int xradio_config(struct ieee80211_hw *dev, u32 changed)
|
||||||
|
+int xradio_config(struct ieee80211_hw *dev, int radio_idx, u32 changed)
|
||||||
|
{
|
||||||
|
int ret = 0;
|
||||||
|
struct xradio_common *hw_priv = dev->priv;
|
||||||
|
@@ -796,7 +796,7 @@ void xradio_wep_key_work(struct work_struct *work)
|
||||||
|
wsm_unlock_tx(hw_priv);
|
||||||
|
}
|
||||||
|
|
||||||
|
-int xradio_set_rts_threshold(struct ieee80211_hw *hw, u32 value)
|
||||||
|
+int xradio_set_rts_threshold(struct ieee80211_hw *hw, int radio_idx, u32 value)
|
||||||
|
{
|
||||||
|
struct xradio_common *hw_priv = hw->priv;
|
||||||
|
int ret = 0;
|
||||||
|
diff --git a/sta.h b/sta.h
|
||||||
|
index 39a023a..5d769f9 100644
|
||||||
|
--- a/sta.h
|
||||||
|
+++ b/sta.h
|
||||||
|
@@ -42,7 +42,7 @@ int xradio_change_interface(struct ieee80211_hw *dev,
|
||||||
|
struct ieee80211_vif *vif,
|
||||||
|
enum nl80211_iftype new_type,
|
||||||
|
bool p2p);
|
||||||
|
-int xradio_config(struct ieee80211_hw *dev, u32 changed);
|
||||||
|
+int xradio_config(struct ieee80211_hw *dev, int radio_idx, u32 changed);
|
||||||
|
int xradio_change_interface(struct ieee80211_hw *dev,
|
||||||
|
struct ieee80211_vif *vif,
|
||||||
|
enum nl80211_iftype new_type,
|
||||||
|
@@ -65,7 +65,7 @@ int xradio_get_stats(struct ieee80211_hw *dev,
|
||||||
|
int xradio_get_tx_stats(struct ieee80211_hw *dev,
|
||||||
|
struct ieee80211_tx_queue_stats *stats);
|
||||||
|
*/
|
||||||
|
-int xradio_set_rts_threshold(struct ieee80211_hw *hw, u32 value);
|
||||||
|
+int xradio_set_rts_threshold(struct ieee80211_hw *hw, int radio_idx, u32 value);
|
||||||
|
|
||||||
|
void xradio_flush(struct ieee80211_hw *hw, struct ieee80211_vif *vif, u32 queues, bool drop);
|
||||||
|
|
||||||
|
--
|
||||||
|
2.43.0
|
||||||
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
From e9f45719716c4633e858495234616450894c5e93 Mon Sep 17 00:00:00 2001
|
||||||
|
From: koshev-msk <kosnt.shell@gmail.com>
|
||||||
|
Date: Mon, 26 Oct 2026 10:40:59 +0300
|
||||||
|
Subject: [PATCH 2/2] Fix more missing prototypes
|
||||||
|
|
||||||
|
- include p2p.h in p2p.c
|
||||||
|
- declare xradio_pending_offchanneltx_work in sta.h
|
||||||
|
---
|
||||||
|
p2p.c | 1 +
|
||||||
|
sta.h | 1 +
|
||||||
|
2 files changed, 2 insertions(+)
|
||||||
|
|
||||||
|
diff --git a/p2p.c b/p2p.c
|
||||||
|
index f32fb43..91be9d5 100644
|
||||||
|
--- a/p2p.c
|
||||||
|
+++ b/p2p.c
|
||||||
|
@@ -1,4 +1,5 @@
|
||||||
|
#include "xradio.h"
|
||||||
|
+#include "p2p.h"
|
||||||
|
|
||||||
|
#ifdef TES_P2P_0002_ROC_RESTART
|
||||||
|
///w, TES_P2P_0002 WorkAround:
|
||||||
|
diff --git a/sta.h b/sta.h
|
||||||
|
index 5d769f9..5daa7e2 100644
|
||||||
|
--- a/sta.h
|
||||||
|
+++ b/sta.h
|
||||||
|
@@ -65,6 +65,7 @@ int xradio_get_stats(struct ieee80211_hw *dev,
|
||||||
|
int xradio_get_tx_stats(struct ieee80211_hw *dev,
|
||||||
|
struct ieee80211_tx_queue_stats *stats);
|
||||||
|
*/
|
||||||
|
+void xradio_pending_offchanneltx_work(struct work_struct *work);
|
||||||
|
int xradio_set_rts_threshold(struct ieee80211_hw *hw, int radio_idx, u32 value);
|
||||||
|
|
||||||
|
void xradio_flush(struct ieee80211_hw *hw, struct ieee80211_vif *vif, u32 queues, bool drop);
|
||||||
|
--
|
||||||
|
2.43.0
|
||||||
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
From be6b427d35803ad7ed2dd33d4e133b5791a99c9c Mon Sep 17 00:00:00 2001
|
||||||
|
From: koshev-msk <kosnt.shell@gmail.com>
|
||||||
|
Date: Mon, 26 Oct 2026 11:08:06 +0300
|
||||||
|
Subject: [PATCH] bh: demote 'missed interrupt' message to dev_dbg
|
||||||
|
|
||||||
|
The BH thread polls the device every HZ/30 and picks up pending data
|
||||||
|
when the edge-triggered IRQ was not seen. This is handled, so there is
|
||||||
|
no reason to warn about it on every occurrence.
|
||||||
|
---
|
||||||
|
bh.c | 2 +-
|
||||||
|
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||||
|
|
||||||
|
diff --git a/bh.c b/bh.c
|
||||||
|
index 592fad2..5663007 100644
|
||||||
|
--- a/bh.c
|
||||||
|
+++ b/bh.c
|
||||||
|
@@ -776,7 +776,7 @@ static int xradio_bh(void *arg)
|
||||||
|
} else if (!status) {
|
||||||
|
/* check if there is data waiting but we missed the interrupt*/
|
||||||
|
if (xradio_bh_rx_availlen(hw_priv) > 0) {
|
||||||
|
- dev_warn(hw_priv->pdev, "missed interrupt\n");
|
||||||
|
+ dev_dbg(hw_priv->pdev, "missed interrupt\n");
|
||||||
|
if(xradio_bh_exchange(hw_priv) < 0){
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
--
|
||||||
|
2.43.0
|
||||||
|
|
||||||
Reference in new issue
Block a user