🍕 Sync 2026-09-14 23:30:37

This commit is contained in:
github-actions[bot] committed 2026-09-14 23:30:37 +08:00
1 parent 0603b094cf
commit 81fe4cd866
214 files changed
+10806 -2630

No files matched your search

@@ -1,6 +1,7 @@
'use strict';
'require baseclass';
'require rpc';
'require uci';
var callGetUnixtime = rpc.declare({
object: 'luci',
@@ -33,9 +34,9 @@ var callCPUInfo = rpc.declare({
method: 'getCPUInfo'
});
var callTempInfo = rpc.declare({
var callCPUUsage = rpc.declare({
object: 'luci',
method: 'getTempInfo'
method: 'getCPUUsage'
});
return baseclass.extend({
@@ -48,9 +49,10 @@ return baseclass.extend({
L.resolveDefault(callSystemInfo(), {}),
L.resolveDefault(callCPUBench(), {}),
L.resolveDefault(callCPUInfo(), {}),
L.resolveDefault(callTempInfo(), {}),
L.resolveDefault(callCPUUsage(), {}),
L.resolveDefault(callLuciVersion(), { revision: _('unknown version'), branch: 'LuCI' }),
L.resolveDefault(callGetUnixtime(), 0)
L.resolveDefault(callGetUnixtime(), 0),
uci.load('system')
]);
},
@@ -59,7 +61,7 @@ return baseclass.extend({
systeminfo = data[1],
cpubench = data[2],
cpuinfo = data[3],
tempinfo = data[4],
cpuusage = data[4],
luciversion = data[5],
unixtime = data[6];
@@ -68,11 +70,16 @@ return baseclass.extend({
var datestr = null;
if (unixtime) {
var date = new Date(unixtime * 1000);
var date = new Date(unixtime * 1000),
zn = uci.get('system', '@system[0]', 'zonename')?.replaceAll(' ', '_') || 'UTC',
ts = uci.get('system', '@system[0]', 'clock_timestyle') || 0,
hc = uci.get('system', '@system[0]', 'clock_hourcycle') || 0;
datestr = new Intl.DateTimeFormat(undefined, {
dateStyle: 'medium',
timeStyle: 'long'
timeStyle: (ts == 0) ? 'long' : 'full',
hourCycle: (hc == 0) ? undefined : hc,
timeZone: zn
}).format(date);
}
@@ -81,6 +88,11 @@ return baseclass.extend({
_('Architecture'), (cpuinfo.cpuinfo || boardinfo.system) + ' ' + cpubench.cpubench,
_('Target Platform'), (L.isObject(boardinfo.release) ? boardinfo.release.target : ''),
_('Firmware Version'), (L.isObject(boardinfo.release) ? boardinfo.release.description + ' / ' : '') + (luciversion || ''),
? '%s%s / '.format(
boardinfo.release.description || '',
boardinfo.release.revision ? boardinfo.release.revision : ''
)
: '') + (luciversion || ''),
_('Kernel Version'), boardinfo.kernel,
_('Local Time'), datestr,
_('Uptime'), systeminfo.uptime ? '%t'.format(systeminfo.uptime) : null,
@@ -91,7 +103,7 @@ return baseclass.extend({
) : null
];
if (tempinfo.tempinfo) {
if (cpuusage.tempinfo) {
fields.splice(6, 0, _('Temperature'));
fields.splice(7, 0, tempinfo.tempinfo);
}
@@ -31,12 +31,13 @@ return baseclass.extend({
var mem = L.isObject(systeminfo.memory) ? systeminfo.memory : {},
swap = L.isObject(systeminfo.swap) ? systeminfo.swap : {};
var fields = [
_('Used'), (mem.total && mem.available) ? (mem.total - mem.free - mem.buffered - mem.cached) : null, mem.total,
];
var available = (mem.available != null) ? mem.available : mem.free;
if (available != null)
available = Math.min(mem.total ?? Infinity, Math.max(0, available + (mem.buffered || 0)));
if (mem.buffered)
fields.push(_('Buffered'), mem.buffered, mem.total);
var fields = [
_('Used'), (mem.total && mem.available) ? (mem.total - available): null, mem.total,
];
if (mem.cached)
fields.push(_('Cached'), mem.cached, mem.total);
@@ -83,6 +83,68 @@ function renderbox(ifc, ipv6, dhcpv6_stats) {
return baseclass.extend({
title: _('Network'),
wanCapacity(networks) {
const lower = new Map(), ports = new Map();
for (const net of networks) {
const l3 = net.getL3Device(), l2 = net.getL2Device();
if (l3 && l2 && l3.getName() != l2.getName())
lower.set(l3.getName(), l2);
}
for (const net of networks) {
let dev = net.getL2Device() || net.getL3Device();
const seen = new Set();
while (dev && !seen.has(dev.getName())) {
seen.add(dev.getName());
// Follow PPPoE and VLAN layers, but retain individual DSA ports.
const parent = lower.get(dev.getName()) || (dev.getType() == 'vlan' ? dev.getParent() : null);
if (!parent || seen.has(parent.getName()))
break;
dev = parent;
}
if (dev) {
const speed = Number(dev.getSpeed());
ports.set(dev.getName(), Number.isFinite(speed) && speed > 0 && speed < 0xffffffff ? speed : 1000);
}
}
return Array.from(ports.values()).reduce((sum, speed) => sum + speed, 0) || 1000;
},
bandwidthBar(value, capacity) {
if (value != null)
value = Number.isFinite(value) ? Math.max(0, Math.min(value, capacity)) : null;
const percent = value != null ? Math.max(0, value / capacity * 100) : 0;
return E('div', {
'class': 'cbi-progressbar',
'title': value != null
? '%.2f Mbps / %s Gbps (%.1f%%)'.format(value, capacity / 1000, percent)
: '- / %s Gbps'.format(capacity / 1000)
}, E('div', { 'style': 'width:%.2f%%'.format(Math.min(100, percent)) }));
},
wanRates(networks, now) {
const previous = this.wanSamples || new Map();
const samples = new Map();
let download = 0, upload = 0, ready = true;
for (const net of networks) {
const dev = net.getL3Device();
if (!dev || samples.has(dev.getName()))
continue;
const name = dev.getName();
const sample = { rx: dev.getRXBytes(), tx: dev.getTXBytes(), time: now };
const old = previous.get(name);
samples.set(name, sample);
if (!old || now <= old.time || sample.rx < old.rx || sample.tx < old.tx) {
ready = false;
continue;
}
// bytes / milliseconds * 8 / 1000 = decimal Mbps.
download += (sample.rx - old.rx) * 8 / (now - old.time) / 1000;
upload += (sample.tx - old.tx) * 8 / (now - old.time) / 1000;
}
this.wanSamples = samples;
return ready && samples.size ? [download, upload] : [null, null];
},
load() {
return Promise.all([
fs.trimmed('/proc/sys/net/netfilter/nf_conntrack_count'),
@@ -96,15 +158,21 @@ return baseclass.extend({
render([ct_count, ct_max, wan_nets, wan6_nets, dhcpv6_stats, onlineusers]) {
const networks = [...wan_nets, ...wan6_nets];
const [download, upload] = this.wanRates(networks, performance.now());
const capacity = this.wanCapacity(networks);
const fields = [
{ label: _('Active Connections'), value: ct_max ? ct_count : null },
{ label: _('Online Users'), value: onlineusers ? onlineusers.onlineusers : null }
{ label: _('Online Users'), value: onlineusers ? onlineusers.onlineusers : null },
{ label: _('Total download bandwidth'), value: this.bandwidthBar(download, capacity) },
{ label: _('Total upload bandwidth'), value: this.bandwidthBar(upload, capacity) }
];
const ctstatus = E('table', { 'class': 'table' });
for (const { label, value } of fields) {
if (label == _('Online Users')) {
if (label != _('Active Connections')) {
ctstatus.appendChild(E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td left', 'width': '33%' }, [ label ]),
E('td', { 'class': 'td left' }, [
@@ -1,9 +1,11 @@
'use strict';
'require baseclass';
'require rpc';
'require fs';
'require uci';
'require network';
'require validation';
'require poll';
const callLuciDHCPLeases = rpc.declare({
object: 'luci-rpc',
@@ -11,32 +13,217 @@ const callLuciDHCPLeases = rpc.declare({
expect: { '': {} }
});
const callClientRates = rpc.declare({
object: 'luci.client-rates',
method: 'get',
params: [ 'addresses' ],
expect: { '': {} }
});
const callClientWeb = rpc.declare({
object: 'luci.client-web',
method: 'get',
expect: { '': {} }
});
const callUfpList = rpc.declare({
object: 'fingerprint',
method: 'fingerprint',
});
return baseclass.extend({
title: _('DHCP Leases'),
title: _('Online Hosts'),
deferFirstLoad: true,
disableCache: true,
// Optional enhancement: never load the helper or query OUIs without the package.
renderHostname(host, mac, fnos) {
const node = E('span', { 'style': 'display:block;text-align:left' }, [ document.createTextNode(host || '-') ]);
if (!L.hasSystemFeature('oui'))
return node;
if (!this.ouiLoader) {
this.ouiLoader = Promise.all([L.resolveDefault(uci.load('oui')), new Promise(function(resolve) {
const script = document.createElement('script');
script.src = L.resource('oui/oui.js') + '?v=7';
script.onload = function() { resolve(window.luciOUI); };
script.onerror = function() { resolve(null); };
document.head.appendChild(script);
})]).then(function(results) {
if (results[1])
results[1].setDevices(uci.sections('oui', 'device'));
return results[1];
});
}
this.ouiLoader.then(function(oui) {
if (oui)
oui.decorate(node, mac, fnos);
});
return node;
},
isMACStatic: {},
isDUIDStatic: {},
isDUIDIAIDStatic: {},
normalizeRateAddress(ip) {
ip = ip.replace(/\/\d+$/, '');
const v4 = validation.parseIPv4(ip);
if (v4)
return v4.join('.');
const v6 = validation.parseIPv6(ip);
return v6 ? v6.map(word => word.toString(16)).join(':') : null;
},
clientAddresses(lease, hints) {
if (lease.rateAddresses)
return lease.rateAddresses;
const host = hints.hosts?.[lease.macaddr?.toUpperCase()] || {};
return Array.from(new Set([
lease.ipaddr, ...L.toArray(lease.ipaddrs), lease.ip6addr, ...L.toArray(lease.ip6addrs),
...L.toArray(host.ipaddrs || host.ipv4), ...L.toArray(host.ip6addrs || host.ipv6)
].filter(Boolean).map(ip => this.normalizeRateAddress(ip)).filter(Boolean)));
},
renderRate(lease, hints, data, direction) {
return this.rateValue(this.clientAddresses(lease, hints), data, direction);
},
rateValue(addresses, data, direction, mac) {
if (direction == 'total') {
const value = data?.totals?.[mac?.toUpperCase()];
return value != null ? [Number(value), '%1024.2mB'.format(value)] : [-1, '-'];
}
let total = 0;
for (const ip of addresses) {
const rate = data?.rates?.[ip];
if (!rate?.ready || (mac && rate.mac && rate.mac != mac.toUpperCase()))
return [ -1, '-' ];
total += Number(rate[direction] || 0);
}
return addresses.length ? [ total, '%1024.1mB/s'.format(total) ] : [ -1, '-' ];
},
rateCell(lease, hints, direction) {
const addresses = this.clientAddresses(lease, hints);
const value = this.rateValue(addresses, this.rateData, direction, lease.macaddr);
return [ value[0], E('span', {
'class': 'luci-client-rate',
'data-addresses': JSON.stringify(addresses),
'data-direction': direction,
'data-mac': lease.macaddr || ''
}, value[1]) ];
},
isFnosClient(client, data) {
return (client.activeAddresses || []).some(address => {
const ip = validation.parseIPv4(address)?.join('.');
const probe = data?.clients?.[ip];
return !!(ip && client.macaddr && probe?.ready &&
probe.mac === client.macaddr.toUpperCase() &&
[5666, 5667].some(port => probe.ports?.includes(port)));
});
},
clientURL(lease, data) {
const ip = validation.parseIPv4(lease.ipaddr || '')?.join('.');
const client = data?.clients?.[ip];
if (!ip || !client?.ready || client.mac != lease.macaddr?.toUpperCase())
return null;
const port = [80, 8080, 5666, 443, 4430, 5667].find(port => client.ports?.includes(port));
if (!port)
return null;
const scheme = [80, 8080, 5666].includes(port) ? 'http' : 'https';
return scheme + '://' + ip + (port == 80 || port == 443 ? '' : ':' + port) + '/';
},
renderClientIP(lease, data) {
const url = this.clientURL(lease, data);
return url ? E('a', { 'href': url, 'target': '_blank', 'rel': 'noopener noreferrer', 'style': 'text-decoration:underline', 'data-value': lease.ipaddr }, lease.ipaddr) : lease.ipaddr;
},
initLeaseTable(table) {
const widget = new L.ui.Table(table);
const update = widget.update;
widget.update = function(...args) {
const result = update.apply(this, args);
this.node.querySelectorAll('.cbi-section-actions').forEach(cell => {
cell.style.setProperty('text-align', 'center', 'important');
});
return result;
};
const derive = widget.deriveSortKey;
widget.deriveSortKey = function(value, index) {
return Array.isArray(value) ? Number(value[0]) : derive.call(this, value, index);
};
const totalIndex = Array.from(table.querySelectorAll('th')).findIndex(th => th.dataset.totalTraffic);
if (!widget.getActiveSortState())
widget.sortState = [totalIndex, true];
L.dom.bindClassInstance(table, widget);
},
updateLeaseOrder(table) {
const widget = L.dom.findClassInstance(table);
if (!widget)
return;
// Keep the table widget's raw keys current for subsequent header clicks.
for (const row of widget.data || [])
for (const value of row)
if (Array.isArray(value) && value[1]?.matches?.('.luci-client-rate'))
value[0] = Number(value[1].closest('td')?.dataset.value ?? -1);
const sorting = widget.getActiveSortState();
if (sorting) {
const rows = Array.from(table.querySelectorAll('tr')).filter(row => row.querySelector('.luci-client-rate'));
const key = row => {
const cell = row.children[sorting[0]];
return cell.hasAttribute('data-value') ? Number(cell.dataset.value) : widget.deriveSortKey(cell.querySelector('a[data-value]') || cell, sorting[0]);
};
const sorted = rows.slice().sort((a, b) => {
const av = key(a), bv = key(b);
const cmp = typeof av == 'number' && typeof bv == 'number' ? av - bv : L.naturalCompare(av, bv);
return sorting[1] ? -cmp : cmp;
});
if (sorted.some((row, i) => row !== rows[i]))
for (const row of sorted)
row.parentElement.appendChild(row);
}
},
refreshRates() {
const cells = Array.from(document.querySelectorAll('.luci-client-rate'));
const addresses = Array.from(new Set(cells.flatMap(cell => JSON.parse(cell.dataset.addresses))));
if (!cells.length)
return Promise.resolve();
return L.resolveDefault(callClientRates(addresses.slice(0, 1024)), {}).then(data => {
this.rateData = data;
// Query again: the normal overview refresh may have replaced the rows.
document.querySelectorAll('.luci-client-rate').forEach(cell => {
const value = this.rateValue(JSON.parse(cell.dataset.addresses), data, cell.dataset.direction, cell.dataset.mac);
cell.textContent = value[1];
cell.closest('td')?.setAttribute('data-value', value[0]);
});
document.querySelectorAll('#status_leases').forEach(table => this.updateLeaseOrder(table));
});
},
load() {
return Promise.all([
callLuciDHCPLeases(),
network.getHostHints(),
L.hasSystemFeature('ufpd') ? callUfpList() : null,
L.resolveDefault(uci.load('dhcp'))
L.resolveDefault(uci.load('dhcp')),
L.resolveDefault(callClientWeb(), {}),
L.resolveDefault(fs.read('/proc/net/arp'), ''),
L.resolveDefault(fs.read('/tmp/luci-client-history.json').then(JSON.parse), {})
]);
},
render([dhcp_leases, host_hints, ufp_list]) {
render([dhcp_leases, host_hints, ufp_list, dhcp_config, web, arp, history]) {
if (!this.ratePoll) {
this.ratePoll = L.bind(this.refreshRates, this);
poll.add(this.ratePoll, 2);
}
if (L.hasSystemFeature('dnsmasq') || L.hasSystemFeature('odhcpd'))
return this.renderLeases(dhcp_leases, host_hints, ufp_list);
return this.renderLeases(dhcp_leases, host_hints, ufp_list, web, arp, history);
return null;
},
@@ -83,14 +270,123 @@ return baseclass.extend({
.then(L.bind(L.ui.changes.displayChanges, L.ui.changes));
},
renderLeases(dhcp_leases, host_hints, macaddr) {
const leases = Array.isArray(dhcp_leases.dhcp_leases) ? dhcp_leases.dhcp_leases : [];
const leases6 = Array.isArray(dhcp_leases.dhcp6_leases) ? dhcp_leases.dhcp6_leases : [];
arpLeases(arp) {
const leases = [];
for (const line of String(arp || '').trim().split(/\n/)) {
const fields = line.trim().split(/\s+/);
if (fields.length != 6 || fields[5] != 'br-lan' ||
!(parseInt(fields[2], 16) & 2) || !validation.parseIPv4(fields[0]) ||
!/^(?:[0-9a-f]{2}:){5}[0-9a-f]{2}$/i.test(fields[3]) ||
fields[3] == '00:00:00:00:00:00' || (parseInt(fields[3].slice(0, 2), 16) & 1))
continue;
leases.push({ ipaddr: fields[0], macaddr: fields[3].toUpperCase(), interface: fields[5] });
}
return leases;
},
mergeLeases(leases, leases6, hints) {
const clients = new Map();
const owners = new Map();
const normalizeMAC = mac => /^(?:[0-9a-f]{2}:){5}[0-9a-f]{2}$/i.test(mac || '') &&
mac != '00:00:00:00:00:00' && !(parseInt(mac.slice(0, 2), 16) & 1)
? mac.toUpperCase() : null;
const addresses = lease => [lease.ipaddr, ...L.toArray(lease.ipaddrs),
lease.ip6addr, ...L.toArray(lease.ip6addrs)].filter(Boolean);
const addOwner = (ip, mac) => {
const address = this.normalizeRateAddress(ip);
if (!address || !mac)
return;
if (!owners.has(address))
owners.set(address, new Set());
owners.get(address).add(mac);
};
for (const [mac, host] of Object.entries(hints.hosts || {}))
for (const ip of [...L.toArray(host.ipaddrs || host.ipv4), ...L.toArray(host.ip6addrs || host.ipv6)])
addOwner(ip, normalizeMAC(mac));
for (const lease of [...leases, ...leases6])
for (const ip of addresses(lease))
addOwner(ip, normalizeMAC(lease.macaddr));
for (const [family, list] of [[4, leases], [6, leases6]]) {
for (const lease of list) {
let mac = normalizeMAC(lease.macaddr);
if (!mac) {
const matches = new Set();
for (const ip of addresses(lease))
for (const owner of owners.get(this.normalizeRateAddress(ip)) || [])
matches.add(owner);
if (matches.size == 1)
mac = matches.values().next().value;
}
// DUID/IAID only deduplicate the same unidentified lease and its history;
// they never join it to a different IPv4 record based on hostname.
const unknown = lease.duid ? [family, lease.duid.toLowerCase(), lease.iaid?.toLowerCase(), lease.interface] :
addresses(lease).length ? [family, ...addresses(lease).map(ip => this.normalizeRateAddress(ip))] : null;
const key = mac || (unknown ? JSON.stringify(unknown) : Symbol());
if (!clients.has(key))
clients.set(key, { macaddr: mac, hostname: lease.hostname,
ipaddrs: [], ip6addrs: [], interfaces: [], leases: [], leases6: [], activeAddresses: [] });
const client = clients.get(key);
client.hostname ||= lease.hostname;
if (lease.interface && !client.interfaces.includes(lease.interface))
client.interfaces.push(lease.interface);
const ips = family == 4 ? client.ipaddrs : client.ip6addrs;
for (const ip of addresses(lease)) {
const address = this.normalizeRateAddress(ip);
if (address && !lease._historical && !client.activeAddresses.includes(address))
client.activeAddresses.push(address);
if (address && !ips.some(value => this.normalizeRateAddress(value) == address))
ips.push(ip);
}
const records = family == 4 ? client.leases : client.leases6;
if (!lease._historical && !records.some(record => family == 4 ? record.ipaddr == lease.ipaddr :
record.duid?.toLowerCase() == lease.duid?.toLowerCase() && record.iaid == lease.iaid))
records.push(Object.assign({}, lease, { macaddr: mac }));
}
}
for (const client of clients.values()) {
const current = { macaddr: client.macaddr, ipaddrs: client.activeAddresses };
client.rateAddresses = client.activeAddresses.length ? this.clientAddresses(current, hints) : [];
}
return Array.from(clients.values());
},
leaseActions(client) {
const actions = [];
for (const [family, leases] of [[4, client.leases], [6, client.leases6]]) {
for (const lease of leases) {
const mac = lease.macaddr?.toLowerCase();
const duid = lease.duid?.toLowerCase();
const iaid = lease.iaid?.toLowerCase();
if (family == 4 ? !mac : !duid)
continue;
const disabled = family == 4 ? this.isMACStatic[mac] :
this.isDUIDStatic[duid] || (iaid && this.isDUIDIAIDStatic[`${duid}%${iaid}`]);
actions.push(E('button', {
'class': 'cbi-button cbi-button-apply',
'click': L.bind(family == 4 ? this.handleCreateStaticLease : this.handleCreateStaticLease6, this, lease),
'data-tooltip': _('Reserve a specific IP address for this device'),
'disabled': disabled || null
}, [_('Reserve IP') + ` (IPv${family})` + (leases.length > 1 ? ` ${family == 4 ? lease.ipaddr : lease.ip6addrs?.[0] || lease.ip6addr || ''}` : '')]));
}
}
return E('div', { 'style': 'display:flex;flex-direction:column;gap:4px;align-items:center' }, actions);
},
renderLeases(dhcp_leases, host_hints, macaddr, web, arp, history) {
const leases = [...(Array.isArray(dhcp_leases.dhcp_leases) ? dhcp_leases.dhcp_leases : []),
...this.arpLeases(arp),
...(Array.isArray(history?.dhcp_leases) ? history.dhcp_leases : []).map(lease => ({ ...lease, _historical: true }))];
const leases6 = [...(Array.isArray(dhcp_leases.dhcp6_leases) ? dhcp_leases.dhcp6_leases : []),
...(Array.isArray(history?.dhcp6_leases) ? history.dhcp6_leases : []).map(lease => ({ ...lease, _historical: true }))];
if (leases.length == 0 && leases6.length == 0)
return E('em', _('No active leases found'));
const machints = host_hints.getMACHints(false);
const isReadonlyView = !L.hasViewPermission();
this.isMACStatic = {};
this.isDUIDStatic = {};
this.isDUIDIAIDStatic = {};
for (const host of uci.sections('dhcp', 'host')) {
for (const mac of L.toArray(host.mac).map(m => m.toLowerCase()))
@@ -106,138 +402,56 @@ return baseclass.extend({
}
};
const clients = this.mergeLeases(leases, leases6, host_hints);
const onlineMACs = new Set(String(arp || '').trim().split(/\n/).map(line => line.trim().split(/\s+/))
.filter(fields => fields.length == 6 && fields[5] == 'br-lan')
.map(fields => fields[3].toUpperCase()));
const table = E('table', { 'id': 'status_leases', 'class': 'table leases' }, [
E('tr', { 'class': 'tr table-titles' }, [
L.hasSystemFeature('odhcpd', 'dhcpv4') ? E('th', { 'class': 'th' }, _('Interface')) : E([]),
E('th', { 'class': 'th' }, _('Hostname')),
E('th', { 'class': 'th' }, _('Online')),
E('th', { 'class': 'th', 'style': 'text-align:left' }, _('Hostname')),
E('th', { 'class': 'th' }, _('IPv4 address')),
E('th', { 'class': 'th' }, _('MAC address')),
E('th', { 'class': 'th' }, _('DUID')),
E('th', { 'class': 'th' }, _('IAID')),
E('th', { 'class': 'th' }, _('Remaining time')),
isReadonlyView ? E([]) : E('th', { 'class': 'th cbi-section-actions' }, _('Static Lease'))
])
]);
cbi_update_table(table, leases.map(L.bind(function(lease) {
let exp;
let vendor;
if (lease.expires === false)
exp = E('em', _('unlimited'));
else if (lease.expires <= 0)
exp = E('em', _('expired'));
else
exp = '%t'.format(lease.expires);
const hint = lease.macaddr ? machints.filter(function(h) { return h[0] == lease.macaddr })[0] : null;
let host = null;
if (hint && lease.hostname && lease.hostname != hint[1])
host = '%s (%s)'.format(lease.hostname, hint[1]);
else if (lease.hostname)
host = lease.hostname;
if (macaddr)
vendor = macaddr[lease.macaddr.toLowerCase()]?.vendor ?? null;
const columns = [
host || '-',
lease.ipaddr,
vendor ? lease.macaddr + ` (${vendor})` : lease.macaddr,
lease.duid || '-',
lease.iaid || '-',
exp,
];
if (L.hasSystemFeature('odhcpd', 'dhcpv4'))
columns.unshift(lease.interface || '-');
if (!isReadonlyView && lease.macaddr != null) {
columns.push(E('button', {
'class': 'cbi-button cbi-button-apply',
'click': L.bind(this.handleCreateStaticLease, this, lease),
'data-tooltip': _('Reserve a specific IP address for this device'),
'disabled': this.isMACStatic[lease.macaddr.toLowerCase()]
}, [ _('Reserve IP') ]));
}
return columns;
}, this)), E('em', _('No active leases found')));
const table6 = E('table', { 'id': 'status_leases6', 'class': 'table leases6' }, [
E('tr', { 'class': 'tr table-titles' }, [
L.hasSystemFeature('odhcpd', 'dhcpv6') ? E('th', { 'class': 'th' }, _('Interface')) : E([]),
E('th', { 'class': 'th' }, _('Hostname')),
E('th', { 'class': 'th' }, _('IPv6 addresses')),
E('th', { 'class': 'th' }, _('DUID')),
E('th', { 'class': 'th' }, _('IAID')),
E('th', { 'class': 'th' }, _('Remaining time')),
isReadonlyView ? E([]) : E('th', { 'class': 'th cbi-section-actions' }, _('Static Lease'))
E('th', { 'class': 'th' }, _('MAC address')),
E('th', { 'class': 'th' }, _('Upload')),
E('th', { 'class': 'th' }, _('Download')),
E('th', { 'class': 'th', 'data-total-traffic': '1' }, _('Total traffic')),
isReadonlyView ? E([]) : E('th', { 'class': 'th cbi-section-actions center' }, _('Static Lease'))
])
]);
cbi_update_table(table6, leases6.map(L.bind(function(lease) {
let exp;
if (lease.expires === false)
exp = E('em', _('unlimited'));
else if (lease.expires <= 0)
exp = E('em', _('expired'));
else
exp = '%t'.format(lease.expires);
const hint = lease.macaddr ? machints.filter(function(h) { return h[0] == lease.macaddr })[0] : null;
let host = null;
if (hint && lease.hostname && lease.hostname != hint[1] && lease.ip6addr != hint[1])
host = '%s (%s)'.format(lease.hostname, hint[1]);
else if (lease.hostname)
host = lease.hostname;
else if (hint)
host = hint[1];
const duid = lease.duid?.toLowerCase();
const iaid = lease.iaid?.toLowerCase();
// Note: "disabled: false" doesn't work
let disabled = null;
if (!duid)
disabled = true;
else if (duid && this.isDUIDStatic[duid])
disabled = true;
else if (duid && iaid && this.isDUIDIAIDStatic[`${duid}%${iaid}`])
disabled = true;
this.initLeaseTable(table);
cbi_update_table(table, clients.map(client => {
const hint = machints.find(h => h[0].toUpperCase() == client.macaddr);
let host = client.hostname || hint?.[1];
if (hint && client.hostname && client.hostname != hint[1])
host = '%s (%s)'.format(client.hostname, hint[1]);
const vendor = macaddr?.[client.macaddr?.toLowerCase()]?.vendor;
const online = onlineMACs.has(client.macaddr);
const status = online ? _('Online') : _('Offline');
const columns = [
host || '-',
lease.ip6addrs ? lease.ip6addrs.join('<br />') : lease.ip6addr,
duid || '-',
iaid || '-',
exp
[online ? 1 : 0, E('span', {
'class': 'luci-client-online',
'role': 'img', 'aria-label': status, 'title': status,
'style': 'display:inline-block;width:10px;height:10px;border-radius:50%;background-color:' + (online ? '#28a745' : '#dc3545')
})],
this.renderHostname(host, client.macaddr, this.isFnosClient(client, web)),
client.ipaddrs.length ? E('div', {}, client.ipaddrs.map(ipaddr =>
E('div', client.activeAddresses.includes(this.normalizeRateAddress(ipaddr)) ? {} : { 'style': 'opacity:.55', 'title': _('Expired') },
client.activeAddresses.includes(this.normalizeRateAddress(ipaddr)) ? this.renderClientIP({ ipaddr, macaddr: client.macaddr }, web) : ipaddr))) : '-',
client.ip6addrs.length ? E('div', { 'style': 'overflow-wrap:anywhere' },
client.ip6addrs.map(ip => E('div', client.activeAddresses.includes(this.normalizeRateAddress(ip)) ? {} : { 'style': 'opacity:.55', 'title': _('Expired') }, ip))) : '-',
vendor ? `${client.macaddr} (${vendor})` : client.macaddr || '-',
this.rateCell(client, host_hints, 'upload'),
this.rateCell(client, host_hints, 'download'),
this.rateCell(client, host_hints, 'total')
];
if (L.hasSystemFeature('odhcpd', 'dhcpv6'))
columns.unshift(lease.interface || '-');
if (!isReadonlyView && lease.duid) {
columns.push(E('button', {
'class': 'cbi-button cbi-button-apply',
'click': L.bind(this.handleCreateStaticLease6, this, lease),
'data-tooltip': _('Reserve a specific IP address for this device'),
'disabled': disabled
}, [ _('Reserve IP') ]));
}
if (!isReadonlyView)
columns.push(this.leaseActions(client));
return columns;
}, this)), E('em', _('No active leases found')));
return E([
E('h3', _('Active DHCPv4 Leases')),
table,
E('h3', _('Active DHCPv6 Leases')),
table6
]);
}), E('em', _('No active leases found')));
this.updateLeaseOrder(table);
return table;
},
});
@@ -285,7 +285,7 @@ return view.extend({
{ name: 'view.status.include.20_memory' },
{ name: 'view.status.include.25_storage' },
{ name: 'view.status.include.30_network' },
{ name: 'view.status.include.40_dhcp', title: _('DHCP Leases'), deferFirstLoad: true },
{ name: 'view.status.include.40_dhcp', title: _('Online Hosts'), deferFirstLoad: true },
{ name: 'view.status.include.50_dsl' },
{ name: 'view.status.include.60_wifi' }
];