🐤 Sync 2026-10-27 12:12:04

This commit is contained in:
github-actions[bot] committed 2026-10-27 12:12:04 +08:00
1 parent 8b63d5d4a9
commit e646593c56
30 files changed
+1281 -416

No files matched your search

+1 -1
View File
@@ -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;
+6 -6
View File
@@ -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"
+6 -6
View File
@@ -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"
+6 -6
View File
@@ -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 "データフラッシュ間隔"
+6 -6
View File
@@ -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"
+6 -6
View File
@@ -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 "Интервал сброса данных"
+147 -6
View File
@@ -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 "周"
+174 -6
View File
@@ -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.
* *
+4
View File
@@ -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');
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+2
View File
@@ -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'
+18
View File
@@ -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
+88
View File
@@ -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