mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-31 07:25:13 +08:00
🍕 Sync 2026-09-14 23:30:37
This commit is contained in:
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' }
|
||||
];
|
||||
|
||||
Reference in new issue
Block a user