🐤 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_VERSION:=0.12.12
PKG_RELEASE:=14
PKG_RELEASE:=15
# Rebuild language packages when a package revision changes without a PO commit.
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.'));
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) {
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];
return names && (names[key] || names.en) || '';
}
function countryName(info) {
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 || '';
}
@@ -119,7 +123,7 @@ return baseclass.extend({
var builds = databases.map(function (db) {
var date = '—';
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(); }
}
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) {
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));
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-port' }, port ? ':' + port : '')
])];
if (description) children.push(E('div', {
'class': 'flows-geoip', title: description.details
}, [
E('div', {}, description.location || description.summary),
description.organization ? E('div', { 'class': 'flows-geoip-network' }, description.organization) : null
]));
if (description) {
var location = [E('div', {}, description.location || description.summary)];
if (description.organization) location.push(E('div', { 'class': 'flows-geoip-network' }, description.organization));
children.push(E('div', {
'class': 'flows-geoip', title: description.details
}, location));
}
return E('td', { 'class': 'flows-addr-cell' }, children);
}
@@ -5,6 +5,7 @@
'require rpc';
'require poll';
'require bandix.devices as devices';
'require bandix.quota as quotaUi';
// 颜色常量定义
var BANDIX_COLOR_UPLOAD = '#f97316'; // 橙色 - 上传/上行
@@ -188,7 +189,7 @@ function getThemeType() {
function formatSize(bytes) {
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));
return parseFloat((bytes / Math.pow(1024, i)).toFixed(2)) + ' ' + units[i];
}
@@ -201,12 +202,12 @@ function formatByterate(bytes_per_sec, unit) {
if (unit === 'bits') {
// 转换为比特单位
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));
return parseFloat((bits_per_sec / Math.pow(1000, i)).toFixed(2)) + ' ' + units[i];
} 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));
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;
// 匹配字节单位
const bytesMatch = speedStr.match(/^([\d.]+)\s*([KMGT]?B\/s)$/i);
const bytesMatch = speedStr.match(/^([\d.]+)\s*((?:[KMGT]i?)?B\/s)$/i);
if (bytesMatch) {
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 = {
'B/S': 1,
'KB/S': 1024,
'MB/S': 1024 * 1024,
'GB/S': 1024 * 1024 * 1024,
'TB/S': 1024 * 1024 * 1024 * 1024
'KIB/S': 1024,
'MIB/S': 1024 * 1024,
'GIB/S': 1024 * 1024 * 1024,
'TIB/S': 1024 * 1024 * 1024 * 1024
};
return value * (bytesMultipliers[unit] || 1);
@@ -477,13 +479,6 @@ var callGetTrafficQuotas = rpc.declare({
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({
object: 'luci.bandix',
method: 'deleteTrafficQuota',
@@ -2051,6 +2046,34 @@ return view.extend({
}
.traffic-quota-input-row .form-input { min-width: 0; flex: 1; }
.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 {
margin-top: 12px;
padding: 10px 12px;
@@ -2093,6 +2116,7 @@ return view.extend({
@media (max-width: 600px) {
.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) {
return E('div', { 'class': 'form-group', 'style': 'margin-bottom: 0;' }, [
E('label', { 'class': 'form-label' }, label),
E('div', { 'class': 'traffic-quota-input-row' }, [
E('input', {
'type': 'number',
'min': '0',
'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' }, [
// One device settings dialog; tabs retain their drafts while switching.
var quotaForm = quotaUi.create({
formatSize: formatSize, writable: !!L.hasViewPermission(),
changed: function () { quotaFormDirty = true; setTrafficQuotaFormDisabled(quotaFormBusy); }
});
var deviceSettingsModal = E('div', { 'class': 'bandix-modal-overlay', 'id': 'device-settings-modal' }, [
E('div', { 'class': 'modal-content bandix-modal', role: 'dialog', 'aria-modal': 'true', 'aria-labelledby': 'device-settings-title' }, [
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-summary', 'id': 'traffic-quota-device-summary' }),
E('div', { 'class': 'traffic-quota-section', 'style': 'margin-top: 0; padding-top: 0; border-top: none;' }, [
E('div', { 'class': 'traffic-quota-header' }, [
E('div', { 'style': 'font-weight: 600;' }, _('Quota Status')),
E('span', { 'class': 'traffic-quota-status', 'id': 'traffic-quota-status' }, _('Not configured'))
E('div', { 'class': 'device-settings-tabs', role: 'tablist', 'aria-label': _('Device Settings') }, [
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('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': 'bandix-modal-body', id: 'device-settings-body' }, [
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', { 'class': 'traffic-quota-grid' }, [
createQuotaInput('minute', _('Minute Quota')),
createQuotaInput('hourly', _('Hourly Quota')),
createQuotaInput('daily', _('Daily Quota')),
createQuotaInput('weekly', _('Weekly Quota')),
createQuotaInput('monthly', _('Monthly Quota')),
createQuotaInput('total', _('Lifetime Quota'))
]),
E('div', { 'class': 'traffic-quota-usage', 'id': 'traffic-quota-usage' }, [
E('div', { 'style': 'opacity: 0.7;' }, _('No quota usage data'))
]),
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', { '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', { id: 'device-settings-quota-panel', role: 'tabpanel', 'aria-labelledby': 'device-settings-quota-tab', hidden: '' }, [quotaForm.root])
]),
E('div', { 'class': 'bandix-modal-footer' }, [
E('button', { 'class': 'cbi-button cbi-button-reset', 'id': 'traffic-quota-modal-close' }, _('Close'))
E('div', { 'class': 'bandix-modal-footer', id: 'device-settings-rate-footer' }, [
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(trafficQuotaModal);
document.body.appendChild(deviceSettingsModal);
// 创建添加规则模态框
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('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('option', { 'value': '1024' }, 'KB/s'),
E('option', { 'value': '1048576' }, 'MB/s'),
E('option', { 'value': '1073741824' }, 'GB/s'),
E('option', { 'value': '125' }, 'Kbps'),
E('option', { 'value': '1024' }, 'KiB/s'),
E('option', { 'value': '1048576' }, 'MiB/s'),
E('option', { 'value': '1073741824' }, 'GiB/s'),
E('option', { 'value': '125' }, 'kbps'),
E('option', { 'value': '125000' }, 'Mbps'),
E('option', { 'value': '125000000' }, 'Gbps')
])
@@ -3573,10 +3553,10 @@ return view.extend({
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('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': '1048576' }, 'MB/s'),
E('option', { 'value': '1073741824' }, 'GB/s'),
E('option', { 'value': '125' }, 'Kbps'),
E('option', { 'value': '1024' }, 'KiB/s'),
E('option', { 'value': '1048576' }, 'MiB/s'),
E('option', { 'value': '1073741824' }, 'GiB/s'),
E('option', { 'value': '125' }, 'kbps'),
E('option', { 'value': '125000' }, 'Mbps'),
E('option', { 'value': '125000000' }, 'Gbps')
])
@@ -3970,21 +3950,21 @@ return view.extend({
downloadUnitSelect.innerHTML = '';
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': '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': '125000000' }, 'Gbps'));
} else {
uploadUnitSelect.appendChild(E('option', { 'value': '1024' }, 'KB/s'));
uploadUnitSelect.appendChild(E('option', { 'value': '1048576' }, 'MB/s'));
uploadUnitSelect.appendChild(E('option', { 'value': '1073741824' }, 'GB/s'));
uploadUnitSelect.appendChild(E('option', { 'value': '1024' }, 'KiB/s'));
uploadUnitSelect.appendChild(E('option', { 'value': '1048576' }, 'MiB/s'));
uploadUnitSelect.appendChild(E('option', { 'value': '1073741824' }, 'GiB/s'));
downloadUnitSelect.appendChild(E('option', { 'value': '1024' }, 'KB/s'));
downloadUnitSelect.appendChild(E('option', { 'value': '1048576' }, 'MB/s'));
downloadUnitSelect.appendChild(E('option', { 'value': '1073741824' }, 'GB/s'));
downloadUnitSelect.appendChild(E('option', { 'value': '1024' }, 'KiB/s'));
downloadUnitSelect.appendChild(E('option', { 'value': '1048576' }, 'MiB/s'));
downloadUnitSelect.appendChild(E('option', { 'value': '1073741824' }, 'GiB/s'));
}
if (editingRule) {
@@ -4206,65 +4186,40 @@ return view.extend({
var currentDevice = null;
var currentQuotaDevice = null;
var quotaFormDirty = false;
var quotaFormBusy = false;
var editingRule = null;
var showRateLimitModal;
var scheduleRulesRequestId = 0;
trafficQuotaModal.querySelectorAll('.traffic-quota-input-row input, .traffic-quota-input-row select').forEach(function (input) {
input.addEventListener(input.tagName === 'SELECT' ? 'change' : 'input', function () {
quotaFormDirty = true;
function selectDeviceSettingsTab(tab, focus) {
document.getElementById('device-settings-body').scrollTop = 0;
['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); }
});
});
// 显示模态框
showRateLimitModal = function (device) {
function showDeviceSettingsModal(device) {
if (quotaFormBusy) return;
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;
quotaFormDirty = false;
var modal = document.getElementById('traffic-quota-bandix-modal');
var deviceSummary = document.getElementById('traffic-quota-device-summary');
var modalContent = [
E('div', { 'class': 'device-summary-name' }, device.host || device.ip4),
E('div', { 'class': 'device-summary-details' }, device.ip4 + ' (' + device.mac + ')')
@@ -4273,23 +4228,29 @@ return view.extend({
if (modalBadges.length) {
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);
modal.classList.add('show');
loadScheduleRules();
loadTrafficQuotaForm();
document.getElementById('device-settings-modal').classList.add('show');
}
function hideTrafficQuotaModal() {
document.getElementById('traffic-quota-bandix-modal').classList.remove('show');
setTimeout(function () {
currentQuotaDevice = null;
quotaFormDirty = false;
}, 300);
function hideDeviceSettingsModal() {
document.getElementById('device-settings-modal').classList.remove('show');
currentDevice = null;
currentQuotaDevice = null;
quotaFormDirty = false;
scheduleRulesRequestId++;
}
// 加载定时限速规则列表
function loadScheduleRules() {
if (!currentDevice) return;
var deviceMac = currentDevice.mac;
var requestId = ++scheduleRulesRequestId;
var rulesList = document.getElementById('schedule-rules-list');
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>';
callGetScheduleLimits().then(function (res) {
if (!currentDevice || currentDevice.mac !== deviceMac || requestId !== scheduleRulesRequestId) return;
// 检查响应格式
if (!res) {
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) {
return rule && rule.mac === currentDevice.mac;
return rule && rule.mac === deviceMac;
});
// 清空列表
@@ -4404,6 +4366,7 @@ return view.extend({
rulesList.appendChild(ruleItem);
});
}).catch(function (error) {
if (!currentDevice || currentDevice.mac !== deviceMac || requestId !== scheduleRulesRequestId) return;
console.error('Failed to load schedule rules:', error);
var errorMsg = _('Failed to load schedule rules');
if (error && error.message) {
@@ -4433,14 +4396,15 @@ return view.extend({
saveButton.classList.add('btn-loading');
saveButton.disabled = true;
callSetHostname(currentDevice.mac, newHostname).then(function (result) {
var savedDevice = currentDevice;
callSetHostname(savedDevice.mac, newHostname).then(function (result) {
// 恢复按钮状态
saveButton.innerHTML = originalText;
saveButton.classList.remove('btn-loading');
saveButton.disabled = false;
// 更新当前设备信息
currentDevice.host = newHostname;
savedDevice.host = newHostname;
// 刷新设备数据
updateDeviceData();
@@ -4457,20 +4421,21 @@ return view.extend({
document.getElementById('hostname-save-btn').addEventListener('click', saveHostname);
document.getElementById('traffic-quota-save-btn').addEventListener('click', function () {
if (!currentQuotaDevice) return;
if (!currentQuotaDevice || !quotaForm.canSave()) return;
var mac = currentQuotaDevice.mac;
var values = ['minute', 'hourly', 'daily', 'weekly', 'monthly', 'total'].map(readQuotaInput);
if (values.some(function (value) { return value === null; })) {
ui.addNotification(null, E('p', {}, _('Quota values must be non-negative and within the supported range.')), 'error');
var payload;
try { payload = quotaForm.read(mac); } catch (error) {
ui.addNotification(null, E('p', {}, error.message), 'error');
return;
}
var button = this;
button.disabled = 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'));
return fetchAllTrafficQuotas(true);
}).then(function () {
setTrafficQuotaFormDisabled(false);
if (currentQuotaDevice && currentQuotaDevice.mac === mac) {
quotaFormDirty = false;
renderTrafficQuotaForm(getTrafficQuotaForDevice(mac), false);
@@ -4478,23 +4443,26 @@ return view.extend({
updateDeviceData();
}).catch(function (error) {
ui.addNotification(null, E('p', {}, _('Failed to save traffic quota') + ': ' + (error.message || error)), 'error');
if (currentQuotaDevice) renderTrafficQuotaForm(getTrafficQuotaForDevice(currentQuotaDevice.mac), true);
}).finally(function () {
button.disabled = false;
setTrafficQuotaFormDisabled(false);
});
});
document.getElementById('traffic-quota-delete-btn').addEventListener('click', function () {
if (!currentQuotaDevice) return;
if (!currentQuotaDevice || quotaFormBusy || !L.hasViewPermission()) return;
var mac = currentQuotaDevice.mac;
showConfirmDialog(
_('Delete Traffic Quota'),
_('Delete this quota and all of its accumulated usage?'),
function () {
if (quotaFormBusy) return;
setTrafficQuotaFormDisabled(true);
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);
}).then(function () {
setTrafficQuotaFormDisabled(false);
if (currentQuotaDevice && currentQuotaDevice.mac === mac) {
quotaFormDirty = false;
renderTrafficQuotaForm(null, false);
@@ -4502,14 +4470,15 @@ return view.extend({
updateDeviceData();
}).catch(function (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('traffic-quota-modal-close').addEventListener('click', hideTrafficQuotaModal);
document.getElementById('bandix-modal-close').addEventListener('click', hideDeviceSettingsModal);
document.getElementById('traffic-quota-modal-close').addEventListener('click', hideDeviceSettingsModal);
// 历史趋势:状态与工具
var latestDevices = [];
@@ -4523,6 +4492,8 @@ return view.extend({
var allTrafficQuotas = [];
var trafficQuotasRequest = null;
var trafficQuotasLoadError = null;
var trafficQuotasReady = false;
var trafficQuotasDirectional = false;
function fetchAllTrafficQuotas(forceRefresh) {
if (trafficQuotasRequest) {
@@ -4540,15 +4511,19 @@ return view.extend({
quotas = res.data.quotas;
} else if (res && Array.isArray(res.quotas)) {
quotas = res.quotas;
} else {
throw new Error(_('Failed to load traffic quotas'));
}
allTrafficQuotas = quotas;
trafficQuotasReady = true;
trafficQuotasDirectional = !!(res.data && res.data.supports_quota_modes === true);
trafficQuotasLoadError = null;
return quotas;
}).catch(function (error) {
console.error('Failed to fetch traffic quotas:', error);
allTrafficQuotas = [];
trafficQuotasLoadError = error && error.message ? error.message : _('Failed to load traffic quotas');
return [];
if (forceRefresh) throw error;
return allTrafficQuotas;
}).finally(function () {
trafficQuotasRequest = null;
});
@@ -4564,77 +4539,21 @@ return view.extend({
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) {
trafficQuotaModal.querySelectorAll('.traffic-quota-input-row input, .traffic-quota-input-row select').forEach(function (input) {
input.disabled = disabled;
});
quotaFormBusy = 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) {
if (!preserveInputs) {
var periods = ['minute', 'hourly', 'daily', 'weekly', 'monthly', 'total'];
periods.forEach(function (period) {
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')))
]));
quotaForm.render(quota, {
ready: trafficQuotasReady, supported: trafficQuotasDirectional, error: trafficQuotasLoadError,
preserveInputs: preserveInputs || quotaFormBusy
});
var deleteBtn = document.getElementById('traffic-quota-delete-btn');
deleteBtn.style.display = quota ? '' : 'none';
setTrafficQuotaFormDisabled(quotaFormBusy);
}
function loadTrafficQuotaForm() {
@@ -4649,23 +4568,7 @@ return view.extend({
}
function buildTrafficQuotaCell(mac) {
var quota = getTrafficQuotaForDevice(mac);
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);
return quotaUi.summary(getTrafficQuotaForDevice(mac), formatSize);
}
// 获取所有定时限速规则
@@ -6379,7 +6282,7 @@ return view.extend({
var isOnline = isDeviceOnline(device);
var deviceQuota = getTrafficQuotaForDevice(device.mac);
var buttonText = 'R';
var buttonText = _('Settings');
var actionButton = E('button', {
'class': 'cbi-button cbi-button-action',
@@ -6387,16 +6290,7 @@ return view.extend({
}, buttonText);
actionButton.addEventListener('click', function () {
showRateLimitModal(device);
});
var quotaButton = E('button', {
'class': 'cbi-button cbi-button-neutral',
'title': _('Traffic Quota')
}, 'Q');
quotaButton.addEventListener('click', function () {
showTrafficQuotaModal(device);
showDeviceSettingsModal(device);
});
var deleteButton = E('button', {
@@ -6433,7 +6327,7 @@ return view.extend({
}),
device.host || '-',
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' }, [
@@ -6658,7 +6552,6 @@ return view.extend({
// 操作
E('td', { 'class': 'device-actions' }, [
actionButton,
quotaButton,
deleteButton
])
]);
@@ -6694,20 +6587,10 @@ return view.extend({
'title': _('Settings')
}, buttonText);
cardActionBtn.addEventListener('click', function () {
showRateLimitModal(device);
showDeviceSettingsModal(device);
});
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 () {
var cardDeleteBtn = E('button', {
'class': 'cbi-button cbi-button-reset',
@@ -7059,7 +6942,7 @@ return view.extend({
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_DAILY = 60 * 24 * 60 * 60 * 1000; // 60 天
@@ -694,8 +694,8 @@ return view.extend({
// 添加网速单位选择选项
o = s.option(form.ListValue, 'speed_unit', _('Speed Units'),
_('Select the speed display unit format'));
o.value('bytes', _('Bytes Units (B/s, KB/s, MB/s)'));
o.value('bits', _('Bits Units (bps, Kbps, Mbps)'));
o.value('bytes', _('Bytes Units (B/s, KiB/s, MiB/s)'));
o.value('bits', _('Bits Units (bps, kbps, Mbps)'));
o.default = 'bytes';
o.rmempty = false;
+6 -6
View File
@@ -653,11 +653,11 @@ msgstr "Unidades de velocidad"
msgid "Select the speed display unit format"
msgstr "Seleccione el formato de unidad de visualización de velocidad"
msgid "Bytes Units (B/s, KB/s, MB/s)"
msgstr "Unidades de bytes (B/s, KB/s, MB/s)"
msgid "Bytes Units (B/s, KiB/s, MiB/s)"
msgstr "Unidades de bytes (B/s, KiB/s, MiB/s)"
msgid "Bits Units (bps, Kbps, Mbps)"
msgstr "Unidades de bits (bps, Kbps, Mbps)"
msgid "Bits Units (bps, kbps, Mbps)"
msgstr "Unidades de bits (bps, kbps, Mbps)"
msgid "Interface Theme"
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"
msgstr "Período de tráfico en tiempo real"
msgid "10 minutes interval uses about 60 KB per device"
msgstr "Intervalo de 10 minutos usa aproximadamente 60 KB por dispositivo"
msgid "10 minutes interval uses about 60 KiB per device"
msgstr "Intervalo de 10 minutos usa aproximadamente 60 KiB por dispositivo"
msgid "Data Flush Interval"
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"
msgstr "Sélectionner le format d'unité d'affichage de la vitesse"
msgid "Bytes Units (B/s, KB/s, MB/s)"
msgstr "Unités en octets (B/s, Ko/s, Mo/s)"
msgid "Bytes Units (B/s, KiB/s, MiB/s)"
msgstr "Unités en octets (B/s, KiB/s, MiB/s)"
msgid "Bits Units (bps, Kbps, Mbps)"
msgstr "Unités en bits (bps, Kbps, Mbps)"
msgid "Bits Units (bps, kbps, Mbps)"
msgstr "Unités en bits (bps, kbps, Mbps)"
msgid "Interface Theme"
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"
msgstr "Période de trafic en temps réel"
msgid "10 minutes interval uses about 60 KB per device"
msgstr "Intervalle de 10 minutes utilise environ 60 Ko par appareil"
msgid "10 minutes interval uses about 60 KiB per device"
msgstr "Intervalle de 10 minutes utilise environ 60 KiB par appareil"
msgid "Data Flush Interval"
msgstr "Intervalle de vidage des données"
+6 -6
View File
@@ -653,11 +653,11 @@ msgstr "速度単位"
msgid "Select the speed display unit format"
msgstr "速度表示の単位形式を選択"
msgid "Bytes Units (B/s, KB/s, MB/s)"
msgstr "バイト単位 (B/s, KB/s, MB/s)"
msgid "Bytes Units (B/s, KiB/s, MiB/s)"
msgstr "バイト単位 (B/s, KiB/s, MiB/s)"
msgid "Bits Units (bps, Kbps, Mbps)"
msgstr "ビット単位 (bps, Kbps, Mbps)"
msgid "Bits Units (bps, kbps, Mbps)"
msgstr "ビット単位 (bps, kbps, Mbps)"
msgid "Interface Theme"
msgstr "インターフェーステーマ"
@@ -692,8 +692,8 @@ msgstr "デバイスのオフライン検出のタイムアウトを設定(秒
msgid "Realtime Traffic Period"
msgstr "リアルタイムトラフィック周期"
msgid "10 minutes interval uses about 60 KB per device"
msgstr "10分間隔でデバイスあたり約60KBを使用"
msgid "10 minutes interval uses about 60 KiB per device"
msgstr "10分間隔でデバイスあたり約60 KiBを使用"
msgid "Data Flush Interval"
msgstr "データフラッシュ間隔"
+6 -6
View File
@@ -653,11 +653,11 @@ msgstr "Jednostki prędkości"
msgid "Select the speed display unit format"
msgstr "Wybierz format jednostki wyświetlania prędkości"
msgid "Bytes Units (B/s, KB/s, MB/s)"
msgstr "Jednostki bajtów (B/s, KB/s, MB/s)"
msgid "Bytes Units (B/s, KiB/s, MiB/s)"
msgstr "Jednostki bajtów (B/s, KiB/s, MiB/s)"
msgid "Bits Units (bps, Kbps, Mbps)"
msgstr "Jednostki bitów (bps, Kbps, Mbps)"
msgid "Bits Units (bps, kbps, Mbps)"
msgstr "Jednostki bitów (bps, kbps, Mbps)"
msgid "Interface Theme"
msgstr "Motyw interfejsu"
@@ -692,8 +692,8 @@ msgstr "Ustaw limit czasu wykrywania urządzeń offline (sekundy). Urządzenia n
msgid "Realtime Traffic Period"
msgstr "Okres ruchu w czasie rzeczywistym"
msgid "10 minutes interval uses about 60 KB per device"
msgstr "Interwał 10 minut używa około 60 KB na urządzenie"
msgid "10 minutes interval uses about 60 KiB per device"
msgstr "Interwał 10 minut używa około 60 KiB na urządzenie"
msgid "Data Flush Interval"
msgstr "Interwał zapisu danych"
+6 -6
View File
@@ -653,11 +653,11 @@ msgstr "Единицы скорости"
msgid "Select the speed display unit format"
msgstr "Выберите формат единиц отображения скорости"
msgid "Bytes Units (B/s, KB/s, MB/s)"
msgstr "Единицы в байтах (B/s, KB/s, MB/s)"
msgid "Bytes Units (B/s, KiB/s, MiB/s)"
msgstr "Единицы в байтах (B/s, KiB/s, MiB/s)"
msgid "Bits Units (bps, Kbps, Mbps)"
msgstr "Единицы в битах (bps, Kbps, Mbps)"
msgid "Bits Units (bps, kbps, Mbps)"
msgstr "Единицы в битах (bps, kbps, Mbps)"
msgid "Interface Theme"
msgstr "Тема интерфейса"
@@ -692,8 +692,8 @@ msgstr "Установить таймаут определения офлайн
msgid "Realtime Traffic Period"
msgstr "Период трафика в реальном времени"
msgid "10 minutes interval uses about 60 KB per device"
msgstr "Интервал 10 минут использует около 60 КБ на устройство"
msgid "10 minutes interval uses about 60 KiB per device"
msgstr "Интервал 10 минут использует около 60 KiB на устройство"
msgid "Data Flush Interval"
msgstr "Интервал сброса данных"
+147 -6
View File
@@ -650,11 +650,11 @@ msgstr "网速单位"
msgid "Select the speed display unit format"
msgstr "选择网速显示的单位格式"
msgid "Bytes Units (B/s, KB/s, MB/s)"
msgstr "字节单位 (B/s, KB/s, MB/s)"
msgid "Bytes Units (B/s, KiB/s, MiB/s)"
msgstr "字节单位 (B/s, KiB/s, MiB/s)"
msgid "Bits Units (bps, Kbps, Mbps)"
msgstr "比特单位 (bps, Kbps, Mbps)"
msgid "Bits Units (bps, kbps, Mbps)"
msgstr "比特单位 (bps, kbps, Mbps)"
msgid "Interface Theme"
msgstr "界面主题"
@@ -689,8 +689,8 @@ msgstr "设置设备离线判断的超时时间(秒)。超过此时间未活
msgid "Realtime Traffic Period"
msgstr "实时流量周期"
msgid "10 minutes interval uses about 60 KB per device"
msgstr "10分钟历史周期,每个设备占用 60KB"
msgid "10 minutes interval uses about 60 KiB per device"
msgstr "10分钟历史周期,每个设备占用 60 KiB"
msgid "Data Flush Interval"
msgstr "数据 flush 间隔"
@@ -1605,3 +1605,144 @@ msgstr "本页匹配"
msgid "No matching connections on this page"
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"
msgstr "選擇网速顯示的單位格式"
msgid "Bytes Units (B/s, KB/s, MB/s)"
msgstr "位元組單位 (B/s, KB/s, MB/s)"
msgid "Bytes Units (B/s, KiB/s, MiB/s)"
msgstr "位元組單位 (B/s, KiB/s, MiB/s)"
msgid "Bits Units (bps, Kbps, Mbps)"
msgstr "位元單位 (bps, Kbps, Mbps)"
msgid "Bits Units (bps, kbps, Mbps)"
msgstr "位元單位 (bps, kbps, Mbps)"
msgid "Interface Theme"
msgstr "界面主題"
@@ -692,8 +692,8 @@ msgstr "設定設備離線判斷的超时時間(秒)。超過此時間未活
msgid "Realtime Traffic Period"
msgstr "即時流量週期"
msgid "10 minutes interval uses about 60 KB per device"
msgstr "10分鐘歷史週期,每个設備佔用 60KB"
msgid "10 minutes interval uses about 60 KiB per device"
msgstr "10分鐘歷史週期,每个設備佔用 60 KiB"
msgid "Data Flush Interval"
msgstr "數据 flush 間隔"
@@ -1578,3 +1578,171 @@ msgstr "本頁符合"
msgid "No matching connections on this page"
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
}
# 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() {
local mac="$1"
if [ -z "$mac" ]; then
@@ -1531,6 +1622,13 @@ case "$1" in
json_add_object "deleteTrafficQuota"
json_add_string "mac"
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_cleanup
@@ -2000,6 +2098,20 @@ case "$1" in
getTrafficQuotas)
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)
mac=""
minute_bytes=""
@@ -70,6 +70,8 @@
"updateScheduleLimit",
"getTrafficQuotas",
"setTrafficQuota",
"setDirectionalTrafficQuota",
"setTrafficQuotaConfig",
"deleteTrafficQuota",
"getGeoipStatus",
"manageGeoip"
@@ -61,6 +61,12 @@ const callLuciWirelessDevices = rpc.declare({
expect: { '': {} }
});
const callWirelessStatus = rpc.declare({
object: 'network.wireless',
method: 'status',
expect: { '': {} }
});
const callLuciBoardJSON = rpc.declare({
object: 'luci-rpc',
method: 'getBoardJSON'
@@ -1448,6 +1454,27 @@ Network = baseclass.extend(/** @lends LuCI.network.prototype */ {
}, 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.
*
+4
View File
@@ -19,6 +19,10 @@ function shellquote(s) {
}
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' &&
match(target, /^(ramips\/mt7621|mediatek\/mt76..|mediatek\/filogic|airoha\/an758[0-9])$/) != null;
}
@@ -62,7 +62,12 @@ function mergeUniqueValues(...lists) {
return merged;
}
let runtimeIfnames = Object.create(null);
function getQcaFallbackIfname(device, section) {
if (Object.prototype.hasOwnProperty.call(runtimeIfnames, section))
return runtimeIfnames[section];
if (/^ath\d+$/.test(String(section || '')))
return section;
@@ -252,13 +257,14 @@ function loadIwinfoResolver(force) {
if (!force && cachedIwinfoResolver != null)
return Promise.resolve(cachedIwinfoResolver);
const configuredDevices = getIwinfoDevicesFromConfig();
const deviceRequest = configuredDevices != null
? Promise.resolve(configuredDevices)
: L.resolveDefault(callIwinfoDevices(), []);
cachedIwinfoResolverPromise = deviceRequest.then((devices) => {
cachedIwinfoResolver = buildIwinfoResolver(devices, configuredDevices != null);
cachedIwinfoResolverPromise = network.getWifiRuntimeIfnames().then((names) => {
runtimeIfnames = names;
const configuredDevices = getIwinfoDevicesFromConfig();
return (configuredDevices != null ? Promise.resolve(configuredDevices)
: L.resolveDefault(callIwinfoDevices(), [])).then((devices) =>
buildIwinfoResolver(devices, configuredDevices != null));
}).then((resolver) => {
cachedIwinfoResolver = resolver;
cachedIwinfoResolverPromise = null;
return cachedIwinfoResolver;
}).catch(() => {
@@ -444,6 +450,9 @@ function isNetworkDisplayUp(radioNet) {
}
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 hwtype = uci.get('wireless', radioDev.getName(), 'type');
const hwmode = uci.get('wireless', radioDev.getName(), 'hwmode') || '';
@@ -608,7 +617,9 @@ function getDisplayEncryption(radioNet) {
function getDisplayBSSID(radioNet) {
// A radio MAC may differ from the BSSID of an individual virtual AP.
const target = cachedIwinfoResolver?.aliasMap[radioNet.getName()];
const candidates = [
cachedIwinfoInfoMap?.[target]?.bssid,
radioNet.getActiveBSSID(),
radioNet.getBSSID(),
uci.get('wireless', radioNet.getName(), 'macaddr')
@@ -1408,7 +1419,7 @@ function getDisplaySignalValue(radioNet, hwtype, is_assoc) {
if (hwtype == 'mt_dbdc' && radioNet.getMode() == 'ap')
return is_assoc ? getDisplayTxPower(radioNet) : null;
if (isQcaWifiHwtype(hwtype) && radioNet.getMode() == 'ap' && is_assoc)
return getDisplayTxPower(radioNet) ?? 30;
return getDisplayTxPower(radioNet);
return radioNet.getSignal();
}
@@ -1748,7 +1759,9 @@ var CBIWifiFrequencyValue = form.Value.extend({
const chval = +cfg_channel;
const allow_auto = (hwtype == 'mt_dbdc' || isQcaWifiHwtype(hwtype) ||
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());
const configDevice = configOnly
? network.getWifiDevicesFromConfig().find((device) => device.getName() == device_section)
@@ -1756,8 +1769,8 @@ var CBIWifiFrequencyValue = form.Value.extend({
return Promise.all([
configOnly ? configDevice : network.getWifiDevice(device_section),
configOnly ? [] : this.callFrequencyList(device_section),
configOnly ? {} : this.callDeviceInfo(device_section),
configOnly ? [] : this.callFrequencyList(queryDevice),
configOnly ? {} : this.callDeviceInfo(queryDevice),
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', [ 'phy' ]), '') : '',
@@ -1851,6 +1864,12 @@ var CBIWifiFrequencyValue = form.Value.extend({
if (!htmode_values.length)
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 (/^11be/.test(hwval) || /^EHT/.test(htval))
hwmode_values = [ 'be' ];
@@ -1904,7 +1923,8 @@ var CBIWifiFrequencyValue = form.Value.extend({
const qca_ht320 = (!!(htmodelist.EHT320 || /^HT320$/.test(htval)) && qca_max_width >= 320);
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 },
'ac', 'AC', { available: qca_has_ac },
'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 = {
'': [
'2g', '2.4 GHz', { available: has_band_channels('2g') },
'5g', '5 GHz', { available: has_band_channels('5g') },
'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') } ],
'ax': [
'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;
if (hwtype == 'qcawifi') {
if (mode == 'ac')
hwmode = '11ac';
else if (mode == 'n')
hwmode = '11ng';
// QCA10 netifd uses the band in hwmode and HT/VHT in htmode.
hwmode = band == '5g' ? '11a' : '11g';
setValue('htmode', mode == 'ac' ? htmode.replace(/^HT/, 'VHT') : (htmode || null));
}
else if (hwtype == 'qcawificfg80211') {
switch (mode) {
@@ -2469,7 +2495,9 @@ var CBIWifiTxPowerValue = form.ListValue.extend({
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.poweroff = this.wifiNetwork ? this.wifiNetwork.getTXPowerOffset() : null;
@@ -2673,10 +2701,12 @@ return view.extend({
callSystemBoard()
]).then((data) => {
this.boardinfo = data[4] || {};
/* MTK capability rates need iwinfo and the PHY MCS/NSS map.
* Populate them before the first render, not the next poll. */
/* Config-only radios need their runtime iwinfo before the first render,
* including QCA VAP names and MTK PHY capability rates. */
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 data;
});
@@ -3314,6 +3344,15 @@ return view.extend({
o.depends('mode', 'ap-wds');
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.depends('mode', 'ap');
o.depends('mode', 'ap-wds');
+1 -1
View File
@@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
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
PKG_RELEASE:=35
PKG_RELEASE:=36
PKG_BUILD_DEPENDS:=iwinfo
PKG_LICENSE:=Apache-2.0
@@ -60,7 +60,12 @@ function pushUnique(list, value) {
list.push(value);
}
let runtimeIfnames = Object.create(null);
function getQcaFallbackIfname(device, section) {
if (Object.prototype.hasOwnProperty.call(runtimeIfnames, section))
return runtimeIfnames[section];
if (/^ath\d+$/.test(String(section || '')))
return section;
@@ -162,7 +167,7 @@ function getRadioDisplayType(radio, iwinfoInfoMap) {
let name = radio.getI18n().replace(/^Generic | Wireless Controller .+$/g, '');
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))
return name;
@@ -241,9 +246,8 @@ function getDisplayEncryption(net) {
}
function getDisplayBSSID(net) {
const bssid = uci.get('wireless', net.getName(), 'macaddr') ||
uci.get('wireless', net.getWifiDeviceName(), 'macaddr') ||
net.getBSSID() || net.getActiveBSSID();
const bssid = net.getActiveBSSID() || net.getBSSID() ||
uci.get('wireless', net.getName(), 'macaddr');
if (bssid && bssid != '00:00:00:00:00:00')
return String(bssid).toUpperCase();
@@ -638,18 +642,21 @@ return baseclass.extend({
loadIwinfoResolver(radios, networks) {
// Vendor radios are queried through configured VIFs, not global probes.
const configOnly = useConfiguredWifi();
const configuredDevices = [];
return network.getWifiRuntimeIfnames().then((names) => {
runtimeIfnames = names;
const configOnly = useConfiguredWifi();
const configuredDevices = [];
for (const iface of uci.sections('wireless', 'wifi-iface')) {
if (iface.disabled == '1' || uci.get('wireless', iface.device, 'disabled') == '1')
continue;
for (const iface of uci.sections('wireless', 'wifi-iface')) {
if (iface.disabled == '1' || uci.get('wireless', iface.device, 'disabled') == '1')
continue;
pushUnique(configuredDevices, getConfiguredIfname(iface));
}
pushUnique(configuredDevices, getConfiguredIfname(iface));
}
return (configOnly ? Promise.resolve(configuredDevices) : L.resolveDefault(callIwinfoDevices(), [])).then((devices) => {
return buildIwinfoResolver(radios, networks, devices);
return (configOnly ? Promise.resolve(configuredDevices) : L.resolveDefault(callIwinfoDevices(), [])).then((devices) => {
return buildIwinfoResolver(radios, networks, devices);
});
}).catch(() => ({
deviceLookup: Object.create(null),
aliasMap: Object.create(null),
+2 -2
View File
@@ -14,8 +14,8 @@ MISE_HASH_x86_64:=3832f39c325e343f81fe3d92b2447c5d1a5eea1bc85092bb7b6c2580622264
MISE_HASH_aarch64:=06186cfbfe947049b21d58575fb0ea800cc26ed1375f20f4b678cb3a9d679437
PKG_NAME:=mise
PKG_VERSION:=2026.10.19
PKG_RELEASE:=42
PKG_VERSION:=2026.10.20
PKG_RELEASE:=43
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_HASH:=skip
+2 -2
View File
@@ -10,12 +10,12 @@ include $(INCLUDE_DIR)/kernel.mk
PKG_NAME:=natflow
PKG_VERSION:=20260930
PKG_RELEASE:=95
PKG_RELEASE:=96
PKG_SOURCE:=$(PKG_VERSION).tar.xz
PKG_SOURCE_URL:=https://github.com/ptpt52/natflow.git
PKG_SOURCE_PROTO:=git
PKG_SOURCE_VERSION:=51496417511f8dc4ebef2618206db11ddedd7773
PKG_SOURCE_VERSION:=7c5028430db40bb6246374f3dbbb73f1e3f4360c
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
PKG_MAINTAINER:=Chen Minqiang <ptpt52@gmail.com>
PKG_LICENSE:=GPL-2.0
+9 -9
View File
@@ -7,26 +7,26 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=node
ifeq ($(CONFIG_NODEJS_22),y)
PKG_VERSION:=22.23.3
PKG_RELEASE:=2
PKG_VERSION:=22.23.4
PKG_RELEASE:=3
PKG_HASH:=skip
PATCH_DIR:=./patches/v22.x
NODEJS_BIN_SUM:=1084aa36196bba4c3a5e69a1ee388a6e4ff729dad09445fbcd434b28fe3c24af
NODEJS_BIN_SUM:=b8492284c76a54baf58d61668dd0e41ee0fc98adabf9503d30f05d08f55b4406
NODE_MODULE_VERSION:=127
else
ifeq ($(CONFIG_NODEJS_26),y)
PKG_VERSION:=26.12.0
PKG_RELEASE:=2
PKG_VERSION:=26.12.1
PKG_RELEASE:=3
PKG_HASH:=skip
PATCH_DIR:=./patches/v26.x
NODEJS_BIN_SUM:=d9f88af72ad34f7ddf8e67289829f1698f54b6254f7d4f54b7453117ff1a3d2c
NODEJS_BIN_SUM:=0fd1eb7770598f52a21995edf6ed35fc690400fa437c2386af329519e9143132
NODE_MODULE_VERSION:=147
else
PKG_VERSION:=24.22.0
PKG_RELEASE:=2
PKG_VERSION:=24.22.1
PKG_RELEASE:=3
PKG_HASH:=skip
PATCH_DIR:=./patches/v24.x
NODEJS_BIN_SUM:=28ea0a438de9a13bd7891fd758eee6c7819ee8af5442dfd8d45bd65e56d4228c
NODEJS_BIN_SUM:=eb6728e77c576a35162c122c45ccadf224d89ff00abc8dae46cc2897c6002d7c
NODE_MODULE_VERSION:=137
endif
endif
+13 -4
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=bandix
PKG_VERSION:=0.12.11
PKG_RELEASE:=6
PKG_VERSION:=0.12.12
PKG_RELEASE:=7
PKG_LICENSE:=Apache-2.0
PKG_MAINTAINER:=timsaya
@@ -13,7 +13,7 @@ include $(INCLUDE_DIR)/package.mk
include $(TOPDIR)/feeds/packages/lang/rust/rust-values.mk
# 二进制文件的文件名和URL
RUST_BANDIX_VERSION:=0.12.11
RUST_BANDIX_VERSION:=0.12.12
RUST_BINARY_FILENAME:=bandix-$(RUST_BANDIX_VERSION)-$(RUSTC_TARGET_ARCH).tar.gz
@@ -65,6 +65,15 @@ else
PKG_HASH:=skip
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)
SECTION:=net
CATEGORY:=Network
@@ -78,7 +87,7 @@ endef
define Build/Prepare
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)
endef
+2
View File
@@ -1,6 +1,7 @@
config bandix 'general'
option iface 'br-lan'
option host '127.0.0.1'
option port '8686'
option data_dir '/usr/share/bandix'
@@ -32,6 +33,7 @@ config bandix 'traffic'
config bandix 'connections'
option enabled '0'
option geoip_dir '/usr/share/bandix-geoip'
config bandix 'dns'
+18
View File
@@ -14,6 +14,7 @@ reload_service() {
start_service() {
local iface
local host
local port
local data_dir
local log_level
@@ -33,6 +34,7 @@ start_service() {
local traffic_exclude_iface_device
local traffic_neighbor_flush_enable
local traffic_neighbor_flush_interval
local geoip_dir
local connections_enabled
local dns_enabled
local dns_max_records
@@ -41,6 +43,8 @@ start_service() {
config_load 'bandix'
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 data_dir 'general' 'data_dir'
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_neighbor_flush_enable 'traffic' 'traffic_neighbor_flush_enable' '0'
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 dns_enabled 'dns' 'enabled'
config_get dns_max_records 'dns' 'dns_max_records'
@@ -150,8 +155,21 @@ start_service() {
args="$args --dns-flush-interval $dns_flush_interval"
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_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 stderr 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