⛄ Sync 2026-04-23 20:32:35

This commit is contained in:
github-actions[bot] committed 2026-04-23 20:32:35 +08:00
1 parent 17738c6f79
commit fe51d2b304
206 files changed
+725671

No files matched your search

@@ -0,0 +1,119 @@
'use strict';
'require baseclass';
'require rpc';
var callGetUnixtime = rpc.declare({
object: 'luci',
method: 'getUnixtime',
expect: { result: 0 }
});
var callLuciVersion = rpc.declare({
object: 'luci',
method: 'getVersion'
});
var callSystemBoard = rpc.declare({
object: 'system',
method: 'board'
});
var callSystemInfo = rpc.declare({
object: 'system',
method: 'info'
});
var callCPUBench = rpc.declare({
object: 'luci',
method: 'getCPUBench'
});
var callCPUInfo = rpc.declare({
object: 'luci',
method: 'getCPUInfo'
});
var callTempInfo = rpc.declare({
object: 'luci',
method: 'getTempInfo'
});
return baseclass.extend({
title: _('System'),
disableCache: true,
load: function() {
return Promise.all([
L.resolveDefault(callSystemBoard(), {}),
L.resolveDefault(callSystemInfo(), {}),
L.resolveDefault(callCPUBench(), {}),
L.resolveDefault(callCPUInfo(), {}),
L.resolveDefault(callTempInfo(), {}),
L.resolveDefault(callLuciVersion(), { revision: _('unknown version'), branch: 'LuCI' }),
L.resolveDefault(callGetUnixtime(), 0)
]);
},
render: function(data) {
var boardinfo = data[0],
systeminfo = data[1],
cpubench = data[2],
cpuinfo = data[3],
tempinfo = data[4],
luciversion = data[5],
unixtime = data[6];
luciversion = luciversion.branch;
var datestr = null;
if (unixtime) {
var date = new Date(unixtime * 1000);
datestr = new Intl.DateTimeFormat(undefined, {
dateStyle: 'medium',
timeStyle: 'long'
}).format(date);
}
var fields = [
_('Hostname'), boardinfo.hostname,
_('Architecture'), (cpuinfo.cpuinfo || boardinfo.system) + ' ' + cpubench.cpubench,
_('Target Platform'), (L.isObject(boardinfo.release) ? boardinfo.release.target : ''),
_('Firmware Version'), (L.isObject(boardinfo.release)
? '%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,
_('Load Average'), Array.isArray(systeminfo.load) ? '%.2f, %.2f, %.2f'.format(
systeminfo.load[0] / 65535.0,
systeminfo.load[1] / 65535.0,
systeminfo.load[2] / 65535.0
) : null
];
if (tempinfo.tempinfo) {
fields.splice(6, 0, _('Temperature'));
fields.splice(7, 0, tempinfo.tempinfo);
}
if (boardinfo.model != "Default string Default string") {
fields.splice(2, 0, _('Model'));
fields.splice(3, 0, boardinfo.model);
}
var table = E('table', { 'class': 'table' });
for (var i = 0; i < fields.length; i += 2) {
table.appendChild(E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td left', 'width': '33%' }, [ fields[i] ]),
E('td', { 'class': 'td left' }, [ (fields[i + 1] != null) ? fields[i + 1] : '?' ])
]));
}
return table;
}
});
@@ -0,0 +1,777 @@
'use strict';
'require baseclass';
'require fs';
'require ui';
'require uci';
'require rpc';
'require network';
'require firewall';
const callGetBuiltinEthernetPorts = rpc.declare({
object: 'luci',
method: 'getBuiltinEthernetPorts',
expect: { result: [] }
});
const callSwconfigPortState = rpc.declare({
object: 'luci',
method: 'getSwconfigPortState',
params: [ 'switch' ],
expect: { result: [] }
});
const callNetworkDeviceStatus = rpc.declare({
object: 'network.device',
method: 'status',
params: [ 'name' ],
expect: { '': {} }
});
function isString(v)
{
return typeof(v) === 'string' && v !== '';
}
function parseBoardPortList(value)
{
if (Array.isArray(value))
return value.filter(isString);
if (isString(value))
return value.trim().split(/[\s,]+/).filter(isString);
return [];
}
function addKnownPort(knownPorts, seenPorts, role, device, extra)
{
if (!isString(device) || seenPorts[device])
return;
seenPorts[device] = true;
const port = Object.assign({
role,
device,
label: device
}, extra || {});
if (!port.netdev && !port.swstate)
port.netdev = network.instantiateDevice(device);
knownPorts.push(port);
}
function parseSwitchPortToken(token)
{
if (!isString(token))
return null;
const match = token.match(/^(\d+)([a-z*]+)?$/i);
return match ? { port: +match[1], flags: (match[2] || '').toLowerCase() } : null;
}
function appendSwitchMemberPorts(sw, ports, cpuPorts, memberPorts)
{
for (const value of ports) {
const token = parseSwitchPortToken(value);
if (!token)
continue;
for (const port of sw.ports) {
if (port.num != token.port)
continue;
if (isString(port.device))
cpuPorts[port.device] = true;
else
memberPorts.push('%s:%d'.format(sw['.name'] || sw.name || '', token.port));
break;
}
}
}
function buildLegacySwitchMappings(mapping, board)
{
const switchVlans = uci.sections('network', 'switch_vlan');
if (!L.isObject(board) || !L.isObject(board.switch))
return;
for (const section of switchVlans) {
const swname = section.device;
const sw = board.switch[swname];
const vid = section.vid || section.vlan;
if (!L.isObject(sw) || !/^[0-9]{1,4}$/.test(vid) || +vid > 4095)
continue;
const ports = L.toArray(section.ports);
const cpuPorts = {};
let memberPorts = [];
sw['.name'] = swname;
appendSwitchMemberPorts(sw, ports, cpuPorts, memberPorts);
memberPorts = memberPorts.filter((port, index) => memberPorts.indexOf(port) === index);
for (const cpuDev in cpuPorts)
mapping['%s.%s'.format(cpuDev, vid)] = memberPorts;
}
for (const swname in board.switch) {
const sw = board.switch[swname];
if (!L.isObject(sw) || !Array.isArray(sw.roles) || !Array.isArray(sw.ports))
continue;
sw['.name'] = swname;
for (const role of sw.roles) {
if (!L.isObject(role) || !isString(role.device))
continue;
const cpuPorts = {};
let memberPorts = [];
appendSwitchMemberPorts(sw, parseBoardPortList(role.ports), cpuPorts, memberPorts);
if (!cpuPorts[role.device])
cpuPorts[role.device] = true;
memberPorts = memberPorts.filter((port, index) => memberPorts.indexOf(port) === index);
if (!mapping[role.device])
mapping[role.device] = [];
for (const port of memberPorts)
if (mapping[role.device].indexOf(port) === -1)
mapping[role.device].push(port);
}
}
}
function getSwitchPortLabel(board, switchName, portNum)
{
const sw = L.isObject(board) && L.isObject(board.switch) ? board.switch[switchName] : null;
if (!L.isObject(sw) || !Array.isArray(sw.ports))
return '%s:%d'.format(switchName, portNum);
for (let i = 0; i < sw.ports.length; i++) {
if (sw.ports[i].num != portNum)
continue;
if (isString(sw.ports[i].label))
return sw.ports[i].label;
if (isString(sw.ports[i].role)) {
let index = 1;
for (let j = 0; j < i; j++)
if (sw.ports[j].role == sw.ports[i].role)
index++;
return sw.ports[i].role + index;
}
break;
}
return '%s:%d'.format(switchName, portNum);
}
function addResolvedPort(knownPorts, seenPorts, role, device, mapping, board, swstate)
{
const resolved = resolveVLANPorts(device, mapping);
for (const value of resolved) {
const match = value.match(/^([^:]+):(\d+)$/);
if (match) {
const switchName = match[1];
const portNum = +match[2];
const switchPorts = swstate[switchName] || {};
addKnownPort(knownPorts, seenPorts, role, value, {
label: getSwitchPortLabel(board, switchName, portNum),
swstate: switchPorts[portNum] || {
port: portNum,
link: false,
speed: 0,
duplex: false,
rx_bytes: 0,
tx_bytes: 0,
rx_packets: 0,
tx_packets: 0
}
});
}
else {
addKnownPort(knownPorts, seenPorts, role, value);
}
}
}
function addBoardNetworkPorts(knownPorts, seenPorts, role, entry, mapping, board, swstate)
{
if (!L.isObject(entry))
return;
const values = parseBoardPortList(entry.ports).concat(parseBoardPortList(entry.ifname));
if (isString(entry.device))
values.push(entry.device);
for (const value of values)
addResolvedPort(knownPorts, seenPorts, role, value, mapping, board, swstate);
}
function resolveVLANChain(ifname, bridges, mapping)
{
while (!mapping[ifname]) {
const m = ifname.match(/^(.+)\.([^.]+)$/);
if (!m)
break;
if (bridges[m[1]]) {
if (bridges[m[1]].vlan_filtering)
mapping[ifname] = bridges[m[1]].vlans[m[2]];
else
mapping[ifname] = bridges[m[1]].ports;
}
else if (/^[0-9]{1,4}$/.test(m[2]) && m[2] <= 4095) {
mapping[ifname] = [ m[1] ];
}
else {
break;
}
ifname = m[1];
}
}
function buildVLANMappings(mapping, board)
{
const bridge_vlans = uci.sections('network', 'bridge-vlan');
const vlan_devices = uci.sections('network', 'device');
const interfaces = uci.sections('network', 'interface');
const bridges = {};
/* find bridge VLANs */
for (let i = 0, s; (s = bridge_vlans[i]) != null; i++) {
if (!isString(s.device) || !/^[0-9]{1,4}$/.test(s.vlan) || +s.vlan > 4095)
continue;
const aliases = L.toArray(s.alias);
const ports = L.toArray(s.ports);
const br = bridges[s.device] = (bridges[s.device] || { ports: [], vlans: {}, vlan_filtering: true });
br.vlans[s.vlan] = [];
for (let p of ports) {
const port = p.replace(/:[ut*]+$/, '');
if (br.ports.indexOf(port) === -1)
br.ports.push(port);
br.vlans[s.vlan].push(port);
}
for (let a of aliases)
if (a != s.vlan)
br.vlans[a] = br.vlans[s.vlan];
}
/* find bridges, VLAN devices */
for (let i = 0, s; (s = vlan_devices[i]) != null; i++) {
if (s.type == 'bridge') {
if (!isString(s.name))
continue;
const ports = L.toArray(s.ports);
const br = bridges[s.name] || (bridges[s.name] = { ports: [], vlans: {}, vlan_filtering: false });
if (s.vlan_filtering == '0')
br.vlan_filtering = false;
else if (s.vlan_filtering == '1')
br.vlan_filtering = true;
for (let p of ports)
if (br.ports.indexOf(p) === -1)
br.ports.push(p);
mapping[s.name] = br.ports;
}
else if (s.type == '8021q' || s.type == '8021ad') {
if (!isString(s.name) || !isString(s.vid) || !isString(s.ifname))
continue;
/* parent device is a bridge */
if (bridges[s.ifname]) {
/* parent bridge is VLAN enabled, device refers to VLAN ports */
if (bridges[s.ifname].vlan_filtering)
mapping[s.name] = bridges[s.ifname].vlans[s.vid];
/* parent bridge is not VLAN enabled, device refers to all bridge ports */
else
mapping[s.name] = bridges[s.ifname].ports;
}
/* parent is a simple netdev */
else {
mapping[s.name] = [ s.ifname ];
}
resolveVLANChain(s.ifname, bridges, mapping);
}
}
/* resolve VLAN tagged interfaces in bridge ports */
for (let brname in bridges) {
for (let bp of bridges[brname].ports)
resolveVLANChain(bp, bridges, mapping);
for (let vid in bridges[brname].vlans)
for (let v of bridges[brname].vlans[vid])
resolveVLANChain(v, bridges, mapping);
}
buildLegacySwitchMappings(mapping, board);
/* find implicit VLAN devices */
for (let i = 0, s; (s = interfaces[i]) != null; i++) {
if (!isString(s.device))
continue;
resolveVLANChain(s.device, bridges, mapping);
}
}
function resolveVLANPorts(ifname, mapping, seen)
{
const ports = [];
if (!seen)
seen = {};
if (mapping[ifname]) {
for (let m of mapping[ifname]) {
if (!seen[m]) {
seen[m] = true;
ports.push.apply(ports, resolveVLANPorts(m, mapping, seen));
}
}
}
else {
ports.push(ifname);
}
return ports.sort(L.naturalCompare);
}
function buildInterfaceMapping(zones, networks, board) {
const vlanmap = {};
const portmap = {};
const netmap = {};
buildVLANMappings(vlanmap, board);
for (let net of networks) {
const l3dev = net.getDevice();
if (!l3dev)
continue;
const ports = resolveVLANPorts(l3dev.getName(), vlanmap);
for (let p of ports) {
portmap[p] = portmap[p] || { networks: [], zones: [] };
portmap[p].networks.push(net);
}
netmap[net.getName()] = net;
}
for (let z of zones) {
const networknames = z.getNetworks();
for (let nn of networknames) {
if (!netmap[nn])
continue;
const l3dev = netmap[nn].getDevice();
if (!l3dev)
continue;
const ports = resolveVLANPorts(l3dev.getName(), vlanmap);
for (let p of ports) {
portmap[p] = portmap[p] || { networks: [], zones: [] };
if (portmap[p].zones.indexOf(z) === -1)
portmap[p].zones.push(z);
}
}
}
return portmap;
}
function formatSpeed(carrier, speed, duplex) {
if ((speed > 0) && duplex) {
const d = (duplex == 'half') ? '\u202f(H)' : '';
const e = E('span', { 'title': _('Speed: %d Mbit/s, Duplex: %s').format(speed, duplex) });
switch (true) {
case (speed < 1000):
e.innerText = '%d\u202fM%s'.format(speed, d);
break;
case (speed == 1000):
e.innerText = '1\u202fGbE' + d;
break;
case (speed >= 1e6 && speed < 1e9):
e.innerText = '%f\u202fTbE'.format(speed / 1e6);
break;
case (speed >= 1e9):
e.innerText = '%f\u202fPbE'.format(speed / 1e9);
break;
default: e.innerText = '%f\u202fGbE'.format(speed / 1000);
}
return e;
}
return carrier ? _('Connected') : _('no link');
}
function getPSEStatus(pse) {
if (!pse)
return null;
const status = pse['c33-power-status'] || pse['podl-power-status'],
power = pse['c33-actual-power'];
return {
status: status,
power: power,
isDelivering: status === 'delivering' && power > 0
};
}
function formatPSEPower(pse) {
if (!pse)
return null;
const status = pse['c33-power-status'] || pse['podl-power-status'],
power = pse['c33-actual-power'];
if (status === 'delivering' && power) {
const watts = (power / 1000).toFixed(1);
/* Format: "⚡ 15.4 W" - lightning bolt + narrow space + watts + narrow space + W */
return E('span', { 'style': 'color:#000' },
[ '\u26a1\ufe0e\u202f%s\u202fW'.format(watts) ]);
}
else if (status === 'searching') {
return E('span', { 'style': 'color:#000' },
[ '\u26a1\ufe0e\u202f' + _('searching') ]);
}
else if (status === 'fault' || status === 'otherfault' || status === 'error') {
return E('span', { 'style': 'color:#d9534f' },
[ '\u26a1\ufe0e\u202f' + _('fault') ]);
}
else if (status === 'disabled') {
return E('span', { 'style': 'color:#888' },
[ '\u26a1\ufe0e\u202f' + _('off') ]);
}
return null;
}
function formatStats(port, pse) {
const stats = port.netdev
? (port.netdev._devstate('stats') || {})
: {
rx_bytes: port.swstate ? port.swstate.rx_bytes : null,
rx_packets: port.swstate ? port.swstate.rx_packets : null,
multicast: null,
rx_errors: null,
rx_dropped: null,
tx_bytes: port.swstate ? port.swstate.tx_bytes : null,
tx_packets: port.swstate ? port.swstate.tx_packets : null,
tx_errors: null,
tx_dropped: null,
collisions: null
};
const items = [
_('Received bytes'), '%1024mB'.format(stats.rx_bytes),
_('Received packets'), '%1000mPkts.'.format(stats.rx_packets),
_('Received multicast'), '%1000mPkts.'.format(stats.multicast),
_('Receive errors'), '%1000mPkts.'.format(stats.rx_errors),
_('Receive dropped'), '%1000mPkts.'.format(stats.rx_dropped),
_('Transmitted bytes'), '%1024mB'.format(stats.tx_bytes),
_('Transmitted packets'), '%1000mPkts.'.format(stats.tx_packets),
_('Transmit errors'), '%1000mPkts.'.format(stats.tx_errors),
_('Transmit dropped'), '%1000mPkts.'.format(stats.tx_dropped),
_('Collisions seen'), stats.collisions
];
if (pse) {
const status = pse['c33-power-status'] || pse['podl-power-status'],
power = pse['c33-actual-power'],
powerClass = pse['c33-power-class'],
powerLimit = pse['c33-available-power-limit'];
items.push(_('PoE status'), status || _('unknown'));
if (power)
items.push(_('PoE power'), '%.1f W'.format(power / 1000));
if (powerClass)
items.push(_('PoE class'), powerClass);
if (powerLimit)
items.push(_('PoE limit'), '%.1f W'.format(powerLimit / 1000));
}
return ui.itemlist(E('span'), items);
}
function getPortSpeed(port)
{
return port.netdev ? port.netdev.getSpeed() : (port.swstate ? port.swstate.speed : null);
}
function getPortDuplex(port)
{
if (port.netdev)
return port.netdev.getDuplex();
return (port.swstate && port.swstate.link) ? (port.swstate.duplex ? 'full' : 'half') : null;
}
function getPortCarrier(port)
{
return port.netdev ? port.netdev.getCarrier() : !!(port.swstate && port.swstate.link);
}
function getPortTXBytes(port)
{
return port.netdev ? port.netdev.getTXBytes() : (port.swstate ? port.swstate.tx_bytes : null);
}
function getPortRXBytes(port)
{
return port.netdev ? port.netdev.getRXBytes() : (port.swstate ? port.swstate.rx_bytes : null);
}
function renderNetworkBadge(network, zonename) {
const l3dev = network.getDevice();
const span = E('span', { 'class': 'ifacebadge', 'style': 'margin:.125em 0' }, [
E('span', {
'class': 'zonebadge',
'title': zonename ? _('Part of zone %q').format(zonename) : _('No zone assigned'),
'style': firewall.getZoneColorStyle(zonename)
}, '\u202f'),
'\u202f', network.getName(), ': '
]);
if (l3dev)
span.appendChild(E('img', {
'title': l3dev.getI18n(),
'src': L.resource('icons/%s%s.svg'.format(l3dev.getType(), l3dev.isUp() ? '' : '_disabled'))
}));
else
span.appendChild(E('em', _('(no interfaces attached)')));
return span;
}
function renderNetworksTooltip(pmap) {
const res = [ null ];
const zmap = {};
const zones = (pmap && Array.isArray(pmap.zones)) ? pmap.zones : [];
const networks = (pmap && Array.isArray(pmap.networks)) ? pmap.networks : [];
for (let pmz of zones) {
const networknames = pmz.getNetworks();
for (let nn of networknames)
zmap[nn] = pmz.getName();
}
for (let pmn of networks)
res.push(E('br'), renderNetworkBadge(pmn, zmap[pmn.getName()]));
if (res.length > 1)
res[0] = N_((res.length - 1) / 2, 'Part of network:', 'Part of networks:');
else
res[0] = _('Port is not part of any network');
return E([], res);
}
return baseclass.extend({
title: _('Port status'),
deferFirstLoad: true,
disableCache: true,
load() {
return Promise.all([
L.resolveDefault(callGetBuiltinEthernetPorts(), []),
L.resolveDefault(fs.read('/etc/board.json'), '{}'),
firewall.getZones(),
network.getNetworks(),
uci.load('network')
]).then((data) => {
const builtinPorts = data[0] || [];
const board = JSON.parse(data[1] || '{}');
const allPorts = new Set();
const swstate = {};
const tasks = [];
builtinPorts.forEach((port) => {
if (port.device)
allPorts.add(port.device);
});
if (allPorts.size === 0 && board.network) {
['lan', 'wan'].forEach((role) => {
if (board.network[role]) {
if (Array.isArray(board.network[role].ports))
board.network[role].ports.forEach((p) => allPorts.add(p));
else if (board.network[role].device)
allPorts.add(board.network[role].device);
}
});
}
if (L.isObject(board) && L.isObject(board.switch)) {
for (const switchName in board.switch) {
tasks.push(L.resolveDefault(callSwconfigPortState(switchName), []).then((ports) => {
swstate[switchName] = ports.reduce((map, port) => {
map[port.port] = port;
return map;
}, {});
}));
}
}
const psePromises = Array.from(allPorts).map((devname) => {
return L.resolveDefault(callNetworkDeviceStatus(devname), {}).then((status) => {
return { name: devname, pse: status.pse || null };
});
});
return Promise.all(tasks).then(() => Promise.all(psePromises)).then((pseResults) => {
const pseMap = {};
pseResults.forEach((result) => {
if (result.pse)
pseMap[result.name] = result.pse;
});
data.push(swstate);
data.push(pseMap);
return data;
});
});
},
render(data) {
const board = JSON.parse(data[1]),
swstate = data[5] || {},
port_map = buildInterfaceMapping(data[2], data[3], board),
pseMap = data[6] || {};
let known_ports = [];
const seenPorts = {};
const vlanMap = {};
buildVLANMappings(vlanMap, board);
if (Array.isArray(data[0]) && data[0].length > 0) {
known_ports = data[0].reduce((ports, port) => {
addResolvedPort(ports, seenPorts, port.role, port.device, vlanMap, board, swstate);
return ports;
}, []);
}
if (L.isObject(board) && L.isObject(board.network)) {
for (let k = 'lan'; k != null; k = (k == 'lan') ? 'wan' : null) {
if (!L.isObject(board.network[k]))
continue;
addBoardNetworkPorts(known_ports, seenPorts, k, board.network[k], vlanMap, board, swstate);
}
}
if (!known_ports.length)
return null;
known_ports.sort(function(a, b) {
return L.naturalCompare(a.label || a.device, b.label || b.device);
});
return E('div', { 'style': 'display:grid;grid-template-columns:repeat(auto-fit, minmax(100px, 1fr));margin-bottom:1em;align-items:center;justify-items:center;text-align:center' }, known_ports.map(function(port) {
const speed = getPortSpeed(port);
const duplex = getPortDuplex(port);
const carrier = getPortCarrier(port);
const pmap = port_map[port.device];
const pzones = (pmap && pmap.zones.length) ? pmap.zones.sort((a, b) => L.naturalCompare(a.getName(), b.getName())) : [ null ];
const pse = pseMap[port.device];
const pseInfo = getPSEStatus(pse);
const psePower = formatPSEPower(pse);
/* Select port icon based on carrier and PSE status */
let portIcon;
if (pseInfo && pseInfo.isDelivering) {
portIcon = carrier ? 'pse_up' : 'pse_down';
} else {
portIcon = carrier ? 'up' : 'down';
}
const statsContent = [
'\u25b2\u202f%1024.1mB'.format(getPortTXBytes(port)),
E('br'),
'\u25bc\u202f%1024.1mB'.format(getPortRXBytes(port))
];
if (psePower) {
statsContent.push(E('br'));
statsContent.push(psePower);
}
statsContent.push(E('span', { 'class': 'cbi-tooltip' }, formatStats(port, pse)));
return E('div', { 'class': 'ifacebox', 'style': 'margin:.25em;width:100px' }, [
E('div', { 'class': 'ifacebox-head', 'style': 'font-weight:bold' }, [ port.label || port.device ]),
E('div', { 'class': 'ifacebox-body' }, [
E('img', { 'src': L.resource('icons/port_%s.svg').format(portIcon) }),
E('br'),
formatSpeed(carrier, speed, duplex)
]),
E('div', { 'class': 'ifacebox-head cbi-tooltip-container', 'style': 'display:flex' }, [
E([], pzones.map(function(zone) {
return E('div', {
'class': 'zonebadge',
'style': 'cursor:help;flex:1;height:3px;opacity:' + (carrier ? 1 : 0.25) + ';' + firewall.getZoneColorStyle(zone)
});
})),
E('span', { 'class': 'cbi-tooltip left' }, [ renderNetworksTooltip(pmap) ])
]),
E('div', { 'class': 'ifacebox-body' }, [
E('div', { 'class': 'cbi-tooltip-container', 'style': 'text-align:left;font-size:80%' }, statsContent)
])
]);
}));
}
});
@@ -0,0 +1,60 @@
'use strict';
'require baseclass';
'require rpc';
var callSystemInfo = rpc.declare({
object: 'system',
method: 'info'
});
function progressbar(value, max, byte) {
var vn = parseInt(value) || 0,
mn = parseInt(max) || 100,
fv = byte ? String.format('%1024.2mB', value) : value,
fm = byte ? String.format('%1024.2mB', max) : max,
pc = Math.floor((100 / mn) * vn);
return E('div', {
'class': 'cbi-progressbar',
'title': '%s / %s (%d%%)'.format(fv, fm, pc)
}, E('div', { 'style': 'width:%.2f%%'.format(pc) }));
}
return baseclass.extend({
title: _('Memory'),
load: function() {
return L.resolveDefault(callSystemInfo(), {});
},
render: function(systeminfo) {
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,
];
if (mem.buffered)
fields.push(_('Buffered'), mem.buffered, mem.total);
if (mem.cached)
fields.push(_('Cached'), mem.cached, mem.total);
if (swap.total > 0)
fields.push(_('Swap used'), swap.total - swap.free, swap.total);
var table = E('table', { 'class': 'table' });
for (var i = 0; i < fields.length; i += 3) {
table.appendChild(E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td left', 'width': '33%' }, [ fields[i] ]),
E('td', { 'class': 'td left' }, [
(fields[i + 1] != null) ? progressbar(fields[i + 1], fields[i + 2], true) : '?'
])
]));
}
return table;
}
});
@@ -0,0 +1,66 @@
'use strict';
'require baseclass';
'require rpc';
var callMountPoints = rpc.declare({
object: 'luci',
method: 'getMountPoints',
expect: { result: [] }
});
function progressbar(value, max, byte) {
var vn = parseInt(value) || 0,
mn = parseInt(max) || 100,
fv = byte ? String.format('%1024.2mB', value) : value,
fm = byte ? String.format('%1024.2mB', max) : max,
pc = Math.floor((100 / mn) * vn);
return E('div', {
'class': 'cbi-progressbar',
'title': '%s / %s (%d%%)'.format(fv, fm, pc)
}, E('div', { 'style': 'width:%.2f%%'.format(pc) }));
}
return baseclass.extend({
title: _('Storage'),
load: function() {
return Promise.all([
L.resolveDefault(callMountPoints(), {}),
]);
},
render: function(data) {
var mounts = data[0],
overlay = null;
for (var i = 0; i < mounts.length; i++) {
if (mounts[i].mount == '/overlay') {
overlay = mounts[i];
break;
}
}
if (!overlay) {
for (var j = 0; j < mounts.length; j++) {
if (mounts[j].mount == '/') {
overlay = mounts[j];
break;
}
}
}
var table = E('table', { 'class': 'table' });
if (overlay) {
table.appendChild(E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td left', 'width': '33%' }, [ overlay.mount ]),
E('td', { 'class': 'td left' }, [
progressbar(overlay.size - overlay.free, overlay.size, true)
])
]));
}
return table;
}
});
@@ -0,0 +1,137 @@
'use strict';
'require baseclass';
'require fs';
'require network';
'require rpc';
'require ui';
/* returns per odhcp6c active interface JSON like:
{"result":{"eth1":{"dhcp_solicit":3,"dhcp_advertise":3,"dhcp_request":3,...}}} */
const callOdhcp6cStats = rpc.declare({
object: 'luci',
method: 'getOdhcp6cStats',
expect: { '': {} },
});
var callOnlineUsers = rpc.declare({
object: 'luci',
method: 'getOnlineUsers'
});
function progressbar(value, max, byte) {
const vn = parseInt(value) || 0;
const mn = parseInt(max) || 100;
const fv = byte ? String.format('%1024.2mB', value) : value;
const fm = byte ? String.format('%1024.2mB', max) : max;
const pc = Math.floor((100 / mn) * vn);
return E('div', {
'class': 'cbi-progressbar',
'title': '%s / %s (%d%%)'.format(fv, fm, pc)
}, E('div', { 'style': 'width:%.2f%%'.format(pc) }));
}
function renderbox(ifc, ipv6, dhcpv6_stats) {
const dev = ifc.getL3Device();
const active = (dev && ifc.getProtocol() != 'none');
const addrs = (ipv6 ? ifc.getIP6Addrs() : ifc.getIPAddrs()) || [];
const dnssrv = (ipv6 ? ifc.getDNS6Addrs() : ifc.getDNSAddrs()) || [];
const expires = ifc.getExpiry();
const uptime = ifc.getUptime();
function addEntries(label, array) {
return Array.isArray(array) ? array.flatMap((item) => [label, item]) : [label, null];
}
function addDhcpv6Stats() {
if (ipv6 && ifc.getProtocol() === 'dhcpv6' && dhcpv6_stats && dhcpv6_stats[dev.device]) {
const arr = [];
for (const [pkt_type, count] of Object.entries(dhcpv6_stats[dev.device]))
arr.push(pkt_type.replace('dhcp_', _('DHCPv6') + ' '), `${count} ${_('pkts', 'packets, abbreviated')}`);
return [_('DHCPv6 Statistics'), E('span', { 'class': 'cbi-tooltip-container'}, [
'📊',
E('span', { 'class': 'cbi-tooltip' }, ui.itemlist(E('span'), arr))
])];
}
return ['', null];
}
return E('div', { class: 'ifacebox' }, [
E('div', { class: 'ifacebox-head center ' + (active ? 'active' : '') },
E('strong', ipv6 ? _('IPv6 Upstream') : _('IPv4 Upstream'))),
E('div', { class: 'ifacebox-body left' }, [
L.itemlist(E('span'), [
_('Protocol'), ifc.getI18n() || E('em', _('Not connected')),
...addEntries(_('Prefix Delegated'), ipv6 ? ifc.getIP6Prefixes?.() : null),
...addEntries(_('Address'), addrs),
_('Gateway'), ipv6 ? (ifc.getGateway6Addr() || '::') : (ifc.getGatewayAddr() || '0.0.0.0'),
...addEntries(_('DNS'), dnssrv),
_('Expires'), (expires != null && expires > -1) ? '%t'.format(expires) : null,
_('Connected'), (uptime > 0) ? '%t'.format(uptime) : null,
...addDhcpv6Stats(),
]),
E('div', {}, renderBadge(
L.resource('icons/%s.svg').format(dev ? dev.getType() : 'ethernet_disabled'), null,
_('Device'), dev ? dev.getI18n() : '-',
_('MAC address'), dev.getMAC())
)
])
]);
}
return baseclass.extend({
title: _('Network'),
load() {
return Promise.all([
fs.trimmed('/proc/sys/net/netfilter/nf_conntrack_count'),
fs.trimmed('/proc/sys/net/netfilter/nf_conntrack_max'),
network.getWANNetworks(),
network.getWAN6Networks(),
callOdhcp6cStats(),
L.resolveDefault(callOnlineUsers(), {})
]);
},
render([ct_count, ct_max, wan_nets, wan6_nets, dhcpv6_stats, onlineusers]) {
const fields = [
{ label: _('Active Connections'), value: ct_max ? ct_count : null },
{ label: _('Online Users'), value: onlineusers ? onlineusers.onlineusers : null }
];
const ctstatus = E('table', { 'class': 'table' });
for (const { label, value } of fields) {
if (label == _('Online Users')) {
ctstatus.appendChild(E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td left', 'width': '33%' }, [ label ]),
E('td', { 'class': 'td left' }, [
(value != null) ? value : '?'
])
]));
} else {
ctstatus.appendChild(E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td left', 'width': '33%' }, [ label ]),
E('td', { 'class': 'td left' }, [
(value != null) ? progressbar(value, ct_max) : '?'
])
]));
}
}
const netstatus = E('div', { 'class': 'network-status-table' });
for (const wan_net of wan_nets)
netstatus.appendChild(renderbox(wan_net, false));
for (const wan6_net of wan6_nets)
netstatus.appendChild(renderbox(wan6_net, true, dhcpv6_stats?.result));
return E([
netstatus,
ctstatus
]);
}
});
@@ -0,0 +1,243 @@
'use strict';
'require baseclass';
'require rpc';
'require uci';
'require network';
'require validation';
const callLuciDHCPLeases = rpc.declare({
object: 'luci-rpc',
method: 'getDHCPLeases',
expect: { '': {} }
});
const callUfpList = rpc.declare({
object: 'fingerprint',
method: 'fingerprint',
});
return baseclass.extend({
title: _('DHCP Leases'),
deferFirstLoad: true,
disableCache: true,
isMACStatic: {},
isDUIDStatic: {},
isDUIDIAIDStatic: {},
load() {
return Promise.all([
callLuciDHCPLeases(),
network.getHostHints(),
L.hasSystemFeature('ufpd') ? callUfpList() : null,
L.resolveDefault(uci.load('dhcp'))
]);
},
render([dhcp_leases, host_hints, ufp_list]) {
if (L.hasSystemFeature('dnsmasq') || L.hasSystemFeature('odhcpd'))
return this.renderLeases(dhcp_leases, host_hints, ufp_list);
return null;
},
handleCreateStaticLease(lease, ev) {
ev.currentTarget.classList.add('spinning');
ev.currentTarget.disabled = true;
ev.currentTarget.blur();
const cfg = uci.add('dhcp', 'host');
uci.set('dhcp', cfg, 'name', lease.hostname);
uci.set('dhcp', cfg, 'ip', lease.ipaddr);
uci.set('dhcp', cfg, 'mac', [lease.macaddr.toLowerCase()]);
return uci.save()
.then(L.bind(L.ui.changes.init, L.ui.changes))
.then(L.bind(L.ui.changes.displayChanges, L.ui.changes));
},
handleCreateStaticLease6(lease, ev) {
ev.currentTarget.classList.add('spinning');
ev.currentTarget.disabled = true;
ev.currentTarget.blur();
const cfg = uci.add('dhcp', 'host');
const ip6addr = lease.ip6addrs?.[0]?.replace(/\/128$/, '');
const ip6arr = ip6addr ? validation.parseIPv6(ip6addr) : null;
// Combine DUID and IAID if both available
// (note that we know that lease.duid is set here)
let duid_iaid = lease.duid.toLowerCase();
if (lease.iaid)
duid_iaid += `%${lease.iaid}`.toLowerCase();
uci.set('dhcp', cfg, 'name', lease.hostname);
uci.set('dhcp', cfg, 'duid', [duid_iaid]);
if (lease.macaddr)
uci.set('dhcp', cfg, 'mac', [lease.macaddr.toLowerCase()]);
if (ip6arr)
uci.set('dhcp', cfg, 'hostid', (ip6arr[6] * 0xFFFF + ip6arr[7]).toString(16));
return uci.save()
.then(L.bind(L.ui.changes.init, L.ui.changes))
.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 : [];
if (leases.length == 0 && leases6.length == 0)
return E('em', _('No active leases found'));
const machints = host_hints.getMACHints(false);
const isReadonlyView = !L.hasViewPermission();
for (const host of uci.sections('dhcp', 'host')) {
for (const mac of L.toArray(host.mac).map(m => m.toLowerCase()))
this.isMACStatic[mac] = true;
for (const duid_iaid of L.toArray(host.duid).map(m => m.toLowerCase())) {
const parts = duid_iaid.split('%').length;
if (parts == 1)
this.isDUIDStatic[duid_iaid] = true;
else if (parts == 2)
this.isDUIDIAIDStatic[duid_iaid] = true;
}
};
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' }, _('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'))
])
]);
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;
const columns = [
host || '-',
lease.ip6addrs ? lease.ip6addrs.join('<br />') : lease.ip6addr,
duid || '-',
iaid || '-',
exp
];
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') ]));
}
return columns;
}, this)), E('em', _('No active leases found')));
return E([
E('h3', _('Active DHCPv4 Leases')),
table,
E('h3', _('Active DHCPv6 Leases')),
table6
]);
},
});
File diff suppressed because it is too large. Load diff