mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-07 20:54:14 +08:00
This commit is contained in:
1 parent
f62cb2dfd1
commit
308b747f8f
12 files changed
+1512
-980
No files matched your search
@@ -11,7 +11,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-vantage
|
||||
PKG_VERSION:=1.0.15
|
||||
PKG_RELEASE:=25
|
||||
PKG_RELEASE:=26
|
||||
PKG_LICENSE:=GPL-3.0-or-later
|
||||
|
||||
LUCI_TITLE:=Vantage dashboard
|
||||
|
||||
@@ -110,6 +110,7 @@
|
||||
--vt-b60: var(--vt-text-2);
|
||||
--vt-band-ink: 60%; /* band colour in a chip's text: 4.5:1 on light surfaces */
|
||||
--vt-series-up: var(--vt-chart-1);
|
||||
--vt-tunnel: var(--vt-chart-4); /* a VPN's pipe on the line: apart from the accent, which the packets in it keep */
|
||||
/* throughput directions: download keeps the uplink colour, upload
|
||||
takes one that no band uses */
|
||||
--vt-dir-dn: var(--vt-chart-1);
|
||||
@@ -818,6 +819,44 @@ h2, h3{
|
||||
.vt-app .vt-link-up .vt-ico { color: var(--vt-text-3); }
|
||||
.vt-app .vt-link-meta { font-size: var(--vt-fs-micro); color: var(--vt-text-3); font-family: var(--vt-mono); white-space: nowrap; margin-top: 2px;
|
||||
padding: 0 6px; border-radius: 6px; background: var(--vt-surface); }
|
||||
/* a full-tunnel VPN over the main line: its wire drawn as a pipe in its
|
||||
own colour around the traffic, the tunnel named under the line's rates */
|
||||
.vt-app .vt-wire-tunnel { stroke-width: 9; stroke-linecap: round; stroke: color-mix(in oklab, var(--vt-tunnel) 24%, transparent); }
|
||||
.vt-app .vt-wire-tunnel.vt-wire-on { stroke: color-mix(in oklab, var(--vt-tunnel) 36%, transparent); }
|
||||
.vt-app .vt-link-tunnel {
|
||||
display: inline-flex; align-items: center; gap: 4px; max-width: 100%; margin-top: 4px; padding: 1px 6px;
|
||||
border: 1px solid color-mix(in oklab, var(--vt-tunnel) 60%, var(--vt-line)); border-radius: 6px;
|
||||
background: var(--vt-surface); color: var(--vt-text-2); font-size: var(--vt-fs-micro); white-space: nowrap;
|
||||
}
|
||||
.vt-app .vt-link-tunnel[hidden] { display: none; }
|
||||
.vt-app .vt-link-tunnel .vt-ico { width: 11px; height: 11px; flex: none; color: var(--vt-tunnel); stroke-width: 2; }
|
||||
.vt-app .vt-link-tunnel-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; font-weight: var(--vt-fw-semibold); }
|
||||
.vt-app .vt-link-tunnel-proto { flex: none; color: var(--vt-text-3); text-transform: uppercase; letter-spacing: .04em; }
|
||||
.vt-app .vt-link-tunnel-proto:empty { display: none; }
|
||||
|
||||
/* more than one line to the internet (failover, mwan3): the main line,
|
||||
its rates and the device in one row as with one, the other lines
|
||||
under the main one, each wired into the device's bottom edge
|
||||
(wires.line). The branches span every row; the rows above and below
|
||||
take what a tall branch column leaves, so the lines stay together. */
|
||||
.vt-app .vt-path-lines { grid-template-rows: 1fr auto auto 1fr; }
|
||||
.vt-app .vt-path-lines > .vt-node-gw { grid-area: 2 / 1; }
|
||||
.vt-app .vt-path-lines > .vt-link-label { grid-area: 2 / 2; }
|
||||
.vt-app .vt-path-lines > .vt-node-ap { grid-area: 2 / 3; }
|
||||
.vt-app .vt-path-lines > .vt-branches { grid-area: 1 / 4 / -1 / 5; }
|
||||
.vt-app .vt-lines { grid-area: 3 / 1; align-self: start; display: flex; flex-direction: column; gap: 12px; padding: 14px 0 6px; min-width: 0; }
|
||||
.vt-app .vt-lines > .vt-node { align-self: stretch; }
|
||||
.vt-app .vt-node-rates { flex-wrap: wrap; column-gap: .3em; row-gap: 0; }
|
||||
.vt-app .vt-node-rates > span:empty { display: none; }
|
||||
/* a line's name ellipsized; the tag goes under it when they don't fit */
|
||||
.vt-app .vt-node-kicker-tag { flex-wrap: wrap; row-gap: 2px; }
|
||||
.vt-app .vt-node-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
||||
/* the main line among others: the default route the kernel takes */
|
||||
.vt-app .vt-node-tag {
|
||||
display: inline-flex; align-items: center; height: 16px; padding: 0 5px; border-radius: 4px;
|
||||
text-transform: none; letter-spacing: 0; font-weight: var(--vt-fw-medium);
|
||||
background: var(--vt-accent-soft); color: var(--vt-accent-text);
|
||||
}
|
||||
|
||||
/* the branches spread over the height the tile gets from its row, so a
|
||||
tall neighbour leaves no empty band above or below the diagram */
|
||||
@@ -836,6 +875,13 @@ h2, h3{
|
||||
.vt-app .vt-branch > .vt-node { justify-self: start; min-width: 200px; }
|
||||
.vt-app .vt-branch-ssids { padding-left: 34px; }
|
||||
.vt-app .vt-branch-ssids .vt-node { max-width: none; align-self: stretch; }
|
||||
/* one column: the main line, its rates, the other lines set in (their
|
||||
wires join the main line's drop into the device), the device */
|
||||
.vt-app .vt-path-lines { grid-template-rows: none; }
|
||||
.vt-app .vt-path-lines > .vt-node-gw, .vt-app .vt-path-lines > .vt-link-label, .vt-app .vt-path-lines > .vt-node-ap,
|
||||
.vt-app .vt-path-lines > .vt-branches, .vt-app .vt-lines { grid-area: auto; }
|
||||
.vt-app .vt-lines { gap: 8px; padding: 0 0 0 34px; }
|
||||
.vt-app .vt-lines > .vt-node { align-self: flex-start; min-width: 200px; max-width: 100%; }
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ 6. system */
|
||||
|
||||
@@ -23,6 +23,159 @@ function own(o, k) { return o != null && Object.prototype.hasOwnProperty.call(o,
|
||||
function obj(v) { return (v && typeof v === 'object' && !Array.isArray(v)) ? v : {}; }
|
||||
function arr(v) { return Array.isArray(v) ? v : []; }
|
||||
|
||||
/* ------------------------------------------------------ default routes */
|
||||
|
||||
/* lines to the internet the path and the chart show beside the main one;
|
||||
any further one stays a network */
|
||||
var MAX_LINES = 4;
|
||||
|
||||
/* netifd protocols, and kernel device types, of tunnels that ride over
|
||||
another line: a full-tunnel VPN is not a second way out. PPTP and L2TP
|
||||
are left out: some ISPs give their access through them. */
|
||||
var TUNNEL = /^(wireguard|openvpn|openconnect|openfortivpn|vpnc|sstp|gre|gretap|grev6|grev6tap|vti|vtiv6|xfrm|ipip|6in4|6to4|6rd|vxlan|vxlan6)$/;
|
||||
var TUNNEL_DEV = /^(wireguard|tun|tap|ipip|ip6tnl|sit|gre|gretap|ip6gre|ip6gretap|vti|vti6|xfrm|vxlan)$/;
|
||||
|
||||
/* netifd lists a route's metric only when the route sets one; the
|
||||
interface's metric applies otherwise. The kernel stores an IPv6
|
||||
metric of 0 as 1024. */
|
||||
function metric(i, r) {
|
||||
var m = r.metric != null ? +r.metric : +i.metric;
|
||||
m = isFinite(m) ? m : 0;
|
||||
return r.target === '::' && m === 0 ? 1024 : m;
|
||||
}
|
||||
/* a default in another table (policy routing: a VPN's own table) is
|
||||
not the one ordinary traffic takes */
|
||||
function mainTable(i, r) {
|
||||
var t = r.table != null ? r.table : i[r.target === '::' ? 'ip6table' : 'ip4table'];
|
||||
return t == null || t === '' || t === 'main' || +t === 254;
|
||||
}
|
||||
/* the interface's best default route in the main table, IPv4 first */
|
||||
function defRoute(i) {
|
||||
return arr(i.route).filter(function(r) {
|
||||
return r && +r.mask === 0 && (r.target === '0.0.0.0' || r.target === '::') && mainTable(i, r);
|
||||
}).sort(function(a, b) {
|
||||
return (a.target === '::') - (b.target === '::') || metric(i, a) - metric(i, b);
|
||||
})[0] || null;
|
||||
}
|
||||
function wanRank(i) {
|
||||
return /^wan$/.test(i.interface) ? 0 : /^wan6$/.test(i.interface) ? 1 : /wan/i.test(i.interface) ? 2 : 3;
|
||||
}
|
||||
function upIfaces(ifaces) {
|
||||
return arr(ifaces).filter(function(i) {
|
||||
return i && i.up && i.interface !== 'loopback' && (i.l3_device || i.device);
|
||||
});
|
||||
}
|
||||
function isTunnel(i, devs) {
|
||||
var d = devs[i.l3_device || i.device];
|
||||
return TUNNEL.test(i.proto || '') || !!(d && (TUNNEL_DEV.test(d.devtype || '') || d.type === 'tunnel'));
|
||||
}
|
||||
/* the interfaces with a default route, the line first: real lines
|
||||
before tunnels (a full-tunnel VPN rides over a line, see ridingTunnel),
|
||||
IPv4 before IPv6 (a softwire's IPv4 route among the IPv4 ones), then
|
||||
the lowest metric, wan/wan6/*wan* on a tie, then the order netifd lists
|
||||
them */
|
||||
function rankRouted(up, devs) {
|
||||
return up.map(function(i, n) {
|
||||
var r = defRoute(i);
|
||||
/* a softwire's device is a tunnel's (ip6tnl), but it carries the line's IPv4, not a VPN */
|
||||
var sw = SOFTWIRE.test(i.proto || '');
|
||||
return r ? { i: i, tunnel: !sw && isTunnel(i, devs), softwire: sw, family: r.target === '::' ? 1 : 0,
|
||||
metric: metric(i, r), wan: wanRank(i), n: n } : null;
|
||||
}).filter(Boolean).sort(function(a, b) {
|
||||
return a.tunnel - b.tunnel || a.family - b.family || a.metric - b.metric || a.wan - b.wan || a.n - b.n;
|
||||
});
|
||||
}
|
||||
/* A full-tunnel VPN over the line: the tunnel holding the default route
|
||||
that ordinary traffic takes, IPv4's whenever there is one, else IPv6's
|
||||
(an IPv4 VPN over an IPv6-only line rides it too). `kernel` is the
|
||||
kernel's own answer (luci.vantage routes: [ { family, dev, metric } ],
|
||||
the route it takes to the internet, at most one a family), which
|
||||
decides: of two routes with the same metric the kernel keeps the one
|
||||
added last, which netifd's list cannot tell, and a VPN daemon may add
|
||||
its own (OpenVPN's tun0) that netifd never lists. Over several next
|
||||
hops (dev null), no tunnel. Without it, only a tunnel whose metric is
|
||||
below that family's best line's, or the family's only default.
|
||||
-> { i, metric } of the tunnel, or null */
|
||||
function ridingTunnel(ifaces, ranked, devs, kernel) {
|
||||
if (Array.isArray(kernel)) {
|
||||
var pick = function(fam) { return kernel.filter(function(x) { return x && x.family === fam && finite(x.metric); })[0]; };
|
||||
var k = pick(4) || pick(6);
|
||||
if (!k || typeof k.dev !== 'string') return null;
|
||||
var i = upIfaces(ifaces).filter(function(x) {
|
||||
return (x.l3_device || x.device) === k.dev && !SOFTWIRE.test(x.proto || '') && isTunnel(x, devs);
|
||||
})[0];
|
||||
return i ? { i: i, metric: k.metric } : null;
|
||||
}
|
||||
var fam = ranked.some(function(r) { return r.family === 0; }) ? 0 : 1;
|
||||
var of = ranked.filter(function(r) { return r.family === fam; });
|
||||
var t = of.filter(function(r) { return r.tunnel; })[0], line = of.filter(function(r) { return !r.tunnel; })[0];
|
||||
return t && (!line || t.metric < line.metric) ? { i: t.i, metric: t.metric } : null;
|
||||
}
|
||||
|
||||
/* The lines to the internet, the main one first: { r, members, metric }
|
||||
with the interfaces each is made of. A softwire (DS-Lite, MAP-E,
|
||||
464XLAT) that wins carries IPv4 over the IPv6 line, so that line is
|
||||
the main one, at the softwire's metric; it is never a line of its own,
|
||||
nor a tunnel unless nothing else has a default. One line per L3
|
||||
device: wan6 on wan's device (or an @wan alias) is part of wan's, and
|
||||
so is an IPv6 interface on the device a PPPoE line runs over (its
|
||||
counters hold the PPP session's). Two IPv4 lines that only share a
|
||||
parent device (two PPPoE sessions on eth0) stay two. The main line
|
||||
carries the full-tunnel VPN over it, if any (tunnel; ridingTunnel). */
|
||||
function groupLines(ifaces, devs, kernel) {
|
||||
var ranked = rankRouted(upIfaces(ifaces), devs), main = ranked[0], groups = [];
|
||||
if (!main) return groups;
|
||||
var at = main.metric, rider = main.tunnel ? null : ridingTunnel(ifaces, ranked, devs, kernel);
|
||||
if (main.softwire)
|
||||
main = ranked.filter(function(r) { return !r.tunnel && !r.softwire && r.family === 1; })[0] || main;
|
||||
/* l3: the members' L3 devices; dev4: the devices their IPv4 runs over */
|
||||
function add(g, r) {
|
||||
g.members.push(r.i);
|
||||
g.l3.push(r.i.l3_device || r.i.device);
|
||||
if (r.family === 0 && typeof r.i.device === 'string') g.dev4.push(r.i.device);
|
||||
}
|
||||
function start(r, m) {
|
||||
var g = { r: r, members: [], metric: m, l3: [], dev4: [] };
|
||||
add(g, r);
|
||||
groups.push(g);
|
||||
}
|
||||
start(main, at);
|
||||
groups[0].tunnel = rider;
|
||||
ranked.forEach(function(r) {
|
||||
if (r === main || r.softwire || r.tunnel) return;
|
||||
var l3 = r.i.l3_device || r.i.device;
|
||||
var g = groups.filter(function(x) { return x.l3.indexOf(l3) >= 0 || (r.family === 1 && x.dev4.indexOf(l3) >= 0); })[0];
|
||||
if (g) add(g, r);
|
||||
else start(r, r.metric);
|
||||
});
|
||||
return groups;
|
||||
}
|
||||
function cidrs(list) {
|
||||
return arr(list).map(function(a) { return a && a.address ? a.address + '/' + a.mask : null; }).filter(Boolean);
|
||||
}
|
||||
/* a line's other interfaces add their addresses and DNS to it (wan6's
|
||||
IPv6 on wan's line) */
|
||||
function absorb(x, g) {
|
||||
function more(list, add) { add.forEach(function(v) { if (list.indexOf(v) < 0) list.push(v); }); }
|
||||
g.members.slice(1).forEach(function(i) {
|
||||
more(x.ipv4, cidrs(i['ipv4-address']));
|
||||
more(x.ipv6, cidrs(i['ipv6-address']));
|
||||
more(x.dns, arr(i['dns-server']).filter(function(v) { return typeof v === 'string'; }));
|
||||
});
|
||||
x.metric = g.metric;
|
||||
x.ifaces = g.members.map(function(i) { return i.interface; });
|
||||
return x;
|
||||
}
|
||||
/* Two lines on DSA ports of one switch: the conduit carries both (and
|
||||
the other ports), so each keeps its own port's counters. */
|
||||
function ownConduits(lines) {
|
||||
var shared = lines.map(function(x, k) {
|
||||
return x.statsDev !== x.dev && lines.some(function(y, j) { return j !== k && (y.statsDev === x.statsDev || y.dev === x.statsDev); });
|
||||
});
|
||||
lines.forEach(function(x, k) { if (shared[k]) x.statsDev = x.dev; });
|
||||
return lines;
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
/* the highest rate a sample may carry; above it a counter was reset */
|
||||
MAX_BPS: MAX_BPS,
|
||||
@@ -213,67 +366,70 @@ return baseclass.extend({
|
||||
return v + (sp.duplex === 'full' ? ' · ' + _('full duplex') : sp.duplex === 'half' ? ' · ' + _('half duplex') : '');
|
||||
},
|
||||
|
||||
/* lines beside the main one that the model lists (m.uplinks) */
|
||||
MAX_LINES: MAX_LINES,
|
||||
|
||||
/* Pick the interface carrying traffic in/out of this device, and the
|
||||
physical device its counters live on. Prefer interfaces with a default
|
||||
route in the main table, as the kernel picks among them: IPv4 before IPv6, then the
|
||||
lowest metric (multi-WAN: the backup uplink has the higher one), with
|
||||
route in the main table, as the kernel picks among them (groupLines):
|
||||
real lines before tunnels, IPv4 before IPv6, then the lowest metric
|
||||
(multi-WAN: the backup uplink has the higher one), with
|
||||
wan/wan6/*wan* priority only on a tie. A softwire (DS-Lite, MAP-E,
|
||||
464XLAT) carries IPv4 over the IPv6 uplink, so it ranks after IPv6:
|
||||
the uplink shown is the line, not the tunnel. Fall back to WAN names, then
|
||||
464XLAT) carries IPv4 over the IPv6 uplink: the uplink shown is
|
||||
the line, not the tunnel. Fall back to WAN names, then
|
||||
the first up interface. A bridge, or a VLAN on one (br-lan.10), is
|
||||
followed to its wired member (wireless members skipped): the port
|
||||
given as `via` (where the bridge learned the gateway), else one with
|
||||
a link, netifd 'ethernet' preferred. `port` is the member known to lead to the
|
||||
gateway: `via`, or the bridge's only wired member; null when it has
|
||||
several and the gateway's port is unknown. DSA with hardware
|
||||
offload uses the conduit's counters. */
|
||||
resolveUplink: function(ifaces, devs, wirelessNames, via) {
|
||||
var self = this;
|
||||
ifaces = arr(ifaces);
|
||||
offload uses the conduit's counters. `kernel`: the kernel's
|
||||
default routes (luci.vantage routes), or null; with a full-tunnel
|
||||
VPN over the line, `tunnel` names it (ridingTunnel). */
|
||||
resolveUplink: function(ifaces, devs, wirelessNames, via, kernel) {
|
||||
devs = obj(devs);
|
||||
|
||||
var up = ifaces.filter(function(i) {
|
||||
return i && i.up && i.interface !== 'loopback' && (i.l3_device || i.device);
|
||||
});
|
||||
function named(list, re) { return list.filter(function(i) { return re.test(i.interface); })[0]; }
|
||||
function preferredWan(list) {
|
||||
return named(list, /^wan$/) || named(list, /^wan6$/) || named(list, /wan/i);
|
||||
var g = groupLines(ifaces, devs, kernel)[0], up = upIfaces(ifaces);
|
||||
if (g) {
|
||||
var x = absorb(this.line(g.r.i, devs, wirelessNames, via), g), t = g.tunnel && g.tunnel.i;
|
||||
x.tunnel = t ? { iface: t.interface, proto: t.proto && t.proto !== 'none' ? t.proto : null, dev: t.l3_device || t.device, metric: g.tunnel.metric } : null;
|
||||
return x;
|
||||
}
|
||||
function wanRank(i) {
|
||||
return /^wan$/.test(i.interface) ? 0 : /^wan6$/.test(i.interface) ? 1 : /wan/i.test(i.interface) ? 2 : 3;
|
||||
}
|
||||
/* netifd lists a route's metric only when the route sets one; the
|
||||
interface's metric applies otherwise. The kernel stores an IPv6
|
||||
metric of 0 as 1024. */
|
||||
function metric(i, r) {
|
||||
var m = r.metric != null ? +r.metric : +i.metric;
|
||||
m = isFinite(m) ? m : 0;
|
||||
return r.target === '::' && m === 0 ? 1024 : m;
|
||||
}
|
||||
/* a default in another table (policy routing: a VPN's own table) is
|
||||
not the one ordinary traffic takes */
|
||||
function mainTable(i, r) {
|
||||
var t = r.table != null ? r.table : i[r.target === '::' ? 'ip6table' : 'ip4table'];
|
||||
return t == null || t === '' || t === 'main' || +t === 254;
|
||||
}
|
||||
function defRoute(i) {
|
||||
return arr(i.route).filter(function(r) {
|
||||
return r && +r.mask === 0 && (r.target === '0.0.0.0' || r.target === '::') && mainTable(i, r);
|
||||
}).sort(function(a, b) {
|
||||
return (a.target === '::') - (b.target === '::') || metric(i, a) - metric(i, b);
|
||||
})[0] || null;
|
||||
}
|
||||
|
||||
function named(re) { return up.filter(function(i) { return re.test(i.interface); })[0]; }
|
||||
/* An AP may retain an up, unmanaged WAN port without any routes. */
|
||||
var routed = up.map(function(i, n) {
|
||||
var r = defRoute(i);
|
||||
return r ? { i: i, family: SOFTWIRE.test(i.proto) ? 2 : r.target === '::' ? 1 : 0, metric: metric(i, r), wan: wanRank(i), n: n } : null;
|
||||
}).filter(Boolean).sort(function(a, b) {
|
||||
return a.family - b.family || a.metric - b.metric || a.wan - b.wan || a.n - b.n;
|
||||
});
|
||||
var pick = routed.length ? routed[0].i : preferredWan(up) || up[0];
|
||||
if (!pick) return null;
|
||||
var pick = named(/^wan$/) || named(/^wan6$/) || named(/wan/i) || up[0];
|
||||
return pick ? this.line(pick, devs, wirelessNames, via) : null;
|
||||
},
|
||||
|
||||
/* The other lines to the internet beside `main` (resolveUplink's):
|
||||
every other up interface with a default route in the main table
|
||||
(failover by metric, mwan3), in the kernel's order, as groupLines
|
||||
makes them: a softwire or a tunnel (WireGuard, OpenVPN, GRE,
|
||||
6in4, ...) rides over another line and is not one of its own; a
|
||||
default in another table (policy routing) is not ordinary
|
||||
traffic's. A WAN that is down has no route: it stays a network
|
||||
until it comes up. On a bridge, `viaOf(next hop)` names the port
|
||||
the bridge learned that gateway on; without it a bridge with
|
||||
several wired ports keeps its own counters.
|
||||
-> [ line, ... ] as resolveUplink's, without main */
|
||||
extraUplinks: function(ifaces, devs, wirelessNames, main, viaOf) {
|
||||
var self = this;
|
||||
if (!main) return [];
|
||||
devs = obj(devs);
|
||||
return groupLines(ifaces, devs).slice(1).filter(function(g) {
|
||||
return g.l3.indexOf(main.l3dev) < 0 && !g.members.some(function(i) { return i.interface === main.iface; });
|
||||
}).map(function(g) {
|
||||
var r = defRoute(g.r.i), hop = r && r.nexthop;
|
||||
return absorb(self.line(g.r.i, devs, wirelessNames, hop && typeof viaOf === 'function' ? viaOf(hop) : null, true), g);
|
||||
});
|
||||
},
|
||||
|
||||
/* one line: the interface, the device its counters live on (see
|
||||
resolveUplink), its addresses and its default route. `own`: on a
|
||||
bridge with several wired ports and the gateway's port unknown,
|
||||
the bridge's (or its VLAN's) own counters, not a guessed port's. */
|
||||
line: function(pick, devs, wirelessNames, via, own) {
|
||||
var self = this;
|
||||
devs = obj(devs);
|
||||
var l3 = pick.l3_device || pick.device, dev = l3, d = devs[dev], port = dev;
|
||||
var isBridge = function(x) { return !!x && (x.type === 'bridge' || x.devtype === 'bridge') && Array.isArray(x['bridge-members']); };
|
||||
var vlanOf = /^(.+)\.\d{1,4}$/.exec(dev);
|
||||
@@ -284,16 +440,19 @@ return baseclass.extend({
|
||||
var eth = wired.filter(function(m) { return devs[m] && devs[m].devtype === 'ethernet'; });
|
||||
var lit = function(m) { return devs[m] && devs[m].carrier === true; };
|
||||
var known = typeof via === 'string' && wired.indexOf(via) >= 0 ? via : null;
|
||||
var next = known || eth.filter(lit)[0] || wired.filter(lit)[0] || eth[0] || wired[0] || members[0];
|
||||
if (next) { dev = next; d = devs[dev] || null; }
|
||||
port = known || (wired.length === 1 ? wired[0] : null);
|
||||
if (!known && own && wired.length > 1) port = null;
|
||||
else {
|
||||
var next = known || eth.filter(lit)[0] || wired.filter(lit)[0] || eth[0] || wired[0] || members[0];
|
||||
if (next) { dev = next; d = devs[dev] || null; }
|
||||
port = known || (wired.length === 1 ? wired[0] : null);
|
||||
}
|
||||
}
|
||||
|
||||
var statsDev = dev;
|
||||
if (d && d.devtype === 'dsa' && d['hw-tc-offload'] && d.conduit && devs[d.conduit])
|
||||
statsDev = d.conduit;
|
||||
|
||||
var route = defRoute(pick), via = route && route.nexthop;
|
||||
var route = defRoute(pick), hop = route && route.nexthop;
|
||||
return {
|
||||
iface: pick.interface,
|
||||
l3dev: l3,
|
||||
@@ -306,22 +465,28 @@ return baseclass.extend({
|
||||
up: !!pick.up,
|
||||
carrier: d ? d.carrier !== false : null,
|
||||
speed: d ? this.parseSpeed(d.speed) : null,
|
||||
ipv4: arr(pick['ipv4-address']).map(function(a) { return a && a.address ? a.address + '/' + a.mask : null; }).filter(Boolean),
|
||||
ipv6: arr(pick['ipv6-address']).map(function(a) { return a && a.address ? a.address + '/' + a.mask : null; }).filter(Boolean),
|
||||
gateway: via && via !== '0.0.0.0' && via !== '::' ? via : null,
|
||||
ipv4: cidrs(pick['ipv4-address']),
|
||||
ipv6: cidrs(pick['ipv6-address']),
|
||||
gateway: hop && hop !== '0.0.0.0' && hop !== '::' ? hop : null,
|
||||
/* a tunnel's default route has no next hop: routed, without a gateway address */
|
||||
defaultRoute: !!route,
|
||||
metric: route ? metric(pick, route) : null,
|
||||
dns: arr(pick['dns-server']).filter(function(s) { return typeof s === 'string'; }),
|
||||
/* the interfaces the line is made of (wan, wan6) */
|
||||
ifaces: [ pick.interface ],
|
||||
stats: d && d.statistics ? d.statistics : null
|
||||
};
|
||||
},
|
||||
|
||||
/* The device's other networks (lan, guest, a VPN, ...): every
|
||||
interface except loopback and the uplink (wan6 shares wan's device).
|
||||
One entry per device, by interface name.
|
||||
interface except loopback and the uplinks shown (a line or a list
|
||||
of them, with the interfaces each is made of; wan6 shares wan's
|
||||
device) and the full-tunnel VPN drawn on the main one. One entry
|
||||
per device, by interface name.
|
||||
-> [ { name, proto, dev, up, ipv4 } ] */
|
||||
networks: function(ifaces, uplink) {
|
||||
networks: function(ifaces, uplinks) {
|
||||
var seen = Object.create(null);
|
||||
uplinks = arr(Array.isArray(uplinks) ? uplinks : [ uplinks ]).filter(Boolean);
|
||||
return arr(ifaces).filter(function(i) {
|
||||
return i && typeof i.interface === 'string' && i.interface !== 'loopback';
|
||||
}).sort(function(a, b) {
|
||||
@@ -337,7 +502,10 @@ return baseclass.extend({
|
||||
ipv4: a4 ? a4.address + '/' + a4.mask : null
|
||||
};
|
||||
}).filter(function(n) {
|
||||
if (uplink && (n.name === uplink.iface || (n.dev && n.dev === uplink.l3dev))) return false;
|
||||
if (uplinks.some(function(u) {
|
||||
return n.name === u.iface || arr(u.ifaces).indexOf(n.name) >= 0 || (n.dev && n.dev === u.l3dev) ||
|
||||
!!(u.tunnel && (n.name === u.tunnel.iface || (n.dev && n.dev === u.tunnel.dev)));
|
||||
})) return false;
|
||||
if (n.dev && seen[n.dev]) return false;
|
||||
if (n.dev) seen[n.dev] = true;
|
||||
return true;
|
||||
@@ -407,6 +575,7 @@ return baseclass.extend({
|
||||
assoc: { ifname: [ station ] }, hapd: { ifname: { clients } },
|
||||
hapdStatus: { ifname: status }, hints, aliases, rdns, mdns,
|
||||
fdb: [ luci.vantage wired host ] (-> wiredHosts),
|
||||
routes: [ luci.vantage routes route ] or null,
|
||||
vendor: fn(mac) -> name|null } */
|
||||
build: function(raw) {
|
||||
raw = raw || {};
|
||||
@@ -491,7 +660,8 @@ return baseclass.extend({
|
||||
radios.push(radio);
|
||||
});
|
||||
|
||||
var uplink = this.resolveUplink(raw.ifaces, devs, wlNames);
|
||||
var kernel = Array.isArray(raw.routes) ? raw.routes : null;
|
||||
var uplink = this.resolveUplink(raw.ifaces, devs, wlNames, null, kernel);
|
||||
var subnets = uplink ? uplink.ipv4 : [];
|
||||
|
||||
function hintOf(mac) { return obj(hints[mac.toUpperCase()] || hints[mac]); }
|
||||
@@ -517,17 +687,25 @@ return baseclass.extend({
|
||||
if (!prev) wiredMacs.push(mac);
|
||||
fdb[mac] = { port: h.port, bridge: typeof h.bridge === 'string' ? h.bridge : null, vlan: finite(h.vlan) ? h.vlan : null, seen: seen };
|
||||
});
|
||||
/* the gateway, by the address the host hints give it; on a bridge
|
||||
with several ports the uplink is the port it was learned on */
|
||||
var gwMac = null;
|
||||
if (uplink && uplink.gateway) Object.keys(hints).forEach(function(k) {
|
||||
var mac = names.normMac(k);
|
||||
if (mac && arr(obj(hints[k]).ipaddrs).concat(arr(obj(hints[k]).ip6addrs)).indexOf(uplink.gateway) >= 0) gwMac = mac;
|
||||
});
|
||||
/* a gateway, by the address the host hints give it; on a bridge
|
||||
with several ports a line is the port it was learned on */
|
||||
function macOf(addr) {
|
||||
var found = null;
|
||||
if (addr) Object.keys(hints).forEach(function(k) {
|
||||
var mac = names.normMac(k);
|
||||
if (mac && arr(obj(hints[k]).ipaddrs).concat(arr(obj(hints[k]).ip6addrs)).indexOf(addr) >= 0) found = mac;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
var gwMac = uplink ? macOf(uplink.gateway) : null;
|
||||
if (uplink && gwMac && fdb[gwMac] && uplink.port !== fdb[gwMac].port) {
|
||||
var via = this.resolveUplink(raw.ifaces, devs, wlNames, fdb[gwMac].port);
|
||||
var via = this.resolveUplink(raw.ifaces, devs, wlNames, fdb[gwMac].port, kernel);
|
||||
if (via && via.port === fdb[gwMac].port) uplink = via;
|
||||
}
|
||||
var uplinks = uplink ? ownConduits([ uplink ].concat(this.extraUplinks(raw.ifaces, devs, wlNames, uplink, function(hop) {
|
||||
var mac = macOf(hop);
|
||||
return mac && fdb[mac] ? fdb[mac].port : null;
|
||||
}).slice(0, MAX_LINES))) : [];
|
||||
/* the network on each bridge (or bridge VLAN: br-lan.10), and its
|
||||
IPv4 subnets for the hosts' addresses */
|
||||
var lans = Object.create(null);
|
||||
@@ -670,7 +848,10 @@ return baseclass.extend({
|
||||
storage: [ this.mount(info.root, _('Root (overlay)')), this.mount(info.tmp, _('Temporary (RAM)')) ].filter(Boolean)
|
||||
},
|
||||
uplink: uplink,
|
||||
networks: this.networks(raw.ifaces, uplink),
|
||||
/* the main line first, then up to MAX_LINES others to the
|
||||
internet; networks leaves out only these */
|
||||
uplinks: uplinks,
|
||||
networks: this.networks(raw.ifaces, uplinks),
|
||||
radios: radios,
|
||||
ssids: ssids,
|
||||
clients: clients,
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
A box is { l, r, t, b, cy } relative to the path element. */
|
||||
|
||||
var STEPS = 32; /* points per curve before resampling */
|
||||
var LANE = 10; /* px between the main line's drop and the other lines', stacked */
|
||||
var GAP = 16; /* px between dots, as the old dash pattern */
|
||||
var STRIDE = 32; /* px a dot travels per period */
|
||||
var PERIOD = { 1: 1600, 2: 1050, 3: 600 }; /* ms per STRIDE by speed */
|
||||
@@ -50,6 +51,48 @@ return baseclass.extend({
|
||||
return { d: 'M' + f1(x1) + ' ' + f1(y1) + 'V' + f1(y2 - rad) + 'Q' + f1(x1) + ' ' + f1(y2) + ' ' + f1(x1 + rad) + ' ' + f1(y2) + 'H' + f1(x2), pts: pts };
|
||||
},
|
||||
|
||||
/* from another line to the internet (a, the k-th beside the main
|
||||
one) into the device (b), clear of the main line's rates: side by
|
||||
side and below the device, along a's middle and up into b's bottom
|
||||
edge, each further line 14 px further in, so the wires nest
|
||||
without crossing; stacked (a phone), from a's left edge down a lane
|
||||
of their own beside the main line's drop (LANE px to its right, so
|
||||
no line runs inside another's wire or a tunnel's pipe) into b's top
|
||||
edge. Otherwise as curve(). */
|
||||
line: function(a, b, k) {
|
||||
var x1, y1, x2, y2, rad, i, s, v, pts = [];
|
||||
if (b.l >= a.r - 2 && a.cy > b.b) {
|
||||
x1 = a.r; y1 = a.cy; y2 = b.b;
|
||||
x2 = Math.min(b.l + 18 + 14 * (k || 0), (b.l + b.r) / 2);
|
||||
rad = Math.min(8, y1 - y2, x2 - x1);
|
||||
if (rad > 0) {
|
||||
pts.push([ x1, y1 ]);
|
||||
for (i = 0; i <= 8; i++) {
|
||||
/* quadratic corner from (x2 - rad, y1) via (x2, y1) to (x2, y1 - rad) */
|
||||
s = i / 8; v = 1 - s;
|
||||
pts.push([ v * v * (x2 - rad) + 2 * v * s * x2 + s * s * x2, v * v * y1 + 2 * v * s * y1 + s * s * (y1 - rad) ]);
|
||||
}
|
||||
pts.push([ x2, y2 ]);
|
||||
return { d: 'M' + f1(x1) + ' ' + f1(y1) + 'H' + f1(x2 - rad) + 'Q' + f1(x2) + ' ' + f1(y1) + ' ' + f1(x2) + ' ' + f1(y1 - rad) + 'V' + f1(y2), pts: pts };
|
||||
}
|
||||
}
|
||||
if (b.l < a.r - 2 && a.b <= b.t) {
|
||||
x1 = a.l; y1 = a.cy; x2 = b.l + 18 + LANE; y2 = b.t;
|
||||
rad = Math.min(8, x1 - x2, y2 - y1);
|
||||
if (rad > 0) {
|
||||
pts.push([ x1, y1 ]);
|
||||
for (i = 0; i <= 8; i++) {
|
||||
/* quadratic corner from (x2 + rad, y1) via (x2, y1) to (x2, y1 + rad) */
|
||||
s = i / 8; v = 1 - s;
|
||||
pts.push([ v * v * (x2 + rad) + 2 * v * s * x2 + s * s * x2, v * v * y1 + 2 * v * s * y1 + s * s * (y1 + rad) ]);
|
||||
}
|
||||
pts.push([ x2, y2 ]);
|
||||
return { d: 'M' + f1(x1) + ' ' + f1(y1) + 'H' + f1(x2 + rad) + 'Q' + f1(x2) + ' ' + f1(y1) + ' ' + f1(x2) + ' ' + f1(y1 + rad) + 'V' + f1(y2), pts: pts };
|
||||
}
|
||||
}
|
||||
return this.curve(a, b);
|
||||
},
|
||||
|
||||
/* points at equal distances along a polyline, about `step` px apart,
|
||||
each with the direction of travel in degrees (unwrapped, so the
|
||||
keyframes never turn the long way round); { pts: [[x, y, deg]], len } */
|
||||
|
||||
@@ -47,6 +47,9 @@ var callThermal = rpc.declare({ object: 'luci.vantage', method: 'thermal', expec
|
||||
/* { hosts, crowded, switches }: crowded when a bridge holds too many
|
||||
entries to list; switches, the swconfig switch ports behind a port */
|
||||
var callWired = rpc.declare({ object: 'luci.vantage', method: 'wired', expect: { '': {} } });
|
||||
/* the route the kernel takes to the internet: which one it kept of two
|
||||
with one metric; { routes: null } when it could not be asked */
|
||||
var callRoutes = rpc.declare({ object: 'luci.vantage', method: 'routes', expect: { '': {} } });
|
||||
/* LuCI's own (its switch page's): link and speed of a switch's ports, in
|
||||
result. The whole reply, and a failed call rejected: a default [] would
|
||||
read as every port down */
|
||||
@@ -296,6 +299,12 @@ var CHECK_ICONS = { uplink: 'port', cpu: 'cpu', memory: 'memory', radios: 'wifi'
|
||||
function txt(el, s) { s = (s == null ? '' : String(s)); if (el.textContent !== s) el.textContent = s; return el; }
|
||||
function setClass(el, c) { if (el.getAttribute('class') !== c) el.setAttribute('class', c); }
|
||||
function setTitle(el, v) { if (el.getAttribute('title') !== v) el.setAttribute('title', v); }
|
||||
/* the throughput history of a line to the internet, the main one too,
|
||||
by its interface: a failover starts the new main line's own history
|
||||
instead of continuing the old one's. ':' keeps it apart from a radio's
|
||||
(a uci section name). */
|
||||
function lineKey(x) { return 'up:' + x.iface; }
|
||||
function histMax(key) { return key.indexOf('up:') === 0 ? HIST_FAST : HIST_MAX; }
|
||||
|
||||
/* an element's own tag and attributes, or a text node's text */
|
||||
function nodeSig(n) {
|
||||
@@ -616,7 +625,9 @@ return view.extend({
|
||||
L.resolveDefault(callWifi(), null),
|
||||
L.resolveDefault(callHints(), {}),
|
||||
L.resolveDefault(callThermal(), []),
|
||||
L.resolveDefault(callWired(), {})
|
||||
L.resolveDefault(callWired(), {}),
|
||||
/* null when the call fails (an older plugin): netifd's metrics decide */
|
||||
L.resolveDefault(callRoutes(), null)
|
||||
]).then(function(r) {
|
||||
var wifiDevs = (r[4] && typeof r[4] === 'object') ? r[4] : null;
|
||||
var wiredReply = (r[7] && typeof r[7] === 'object') ? r[7] : {};
|
||||
@@ -669,6 +680,9 @@ return view.extend({
|
||||
Object.keys(swState).forEach(function(n) { switchState[n] = swState[n].ports; });
|
||||
return { info: r[0] || {}, stat: r[1] || '', ifaces: r[2] || [], devs: r[3] || {}, wifi: wifiDevs, hints: r[5] || {}, thermal: r[6] || [],
|
||||
fdb: Array.isArray(wiredReply.hosts) ? wiredReply.hosts : [], fdbCrowded: wiredReply.crowded === true, switches: switches, switchState: switchState,
|
||||
/* no answer (an older plugin, a lookup not sent, a failed call)
|
||||
keeps the last one: no tunnel blinking off */
|
||||
routes: r[8] && Array.isArray(r[8].routes) ? (self.kernelRoutes = r[8].routes) : (self.kernelRoutes || null),
|
||||
assoc: assoc, hapd: hapd, hapdStatus: hapdStatus, iwinfo: iwinfo, at: Date.now(), started: started };
|
||||
});
|
||||
});
|
||||
@@ -682,7 +696,8 @@ return view.extend({
|
||||
var net = Object.create(null);
|
||||
if (stored.net && typeof stored.net === 'object')
|
||||
Object.keys(stored.net).forEach(function(k) {
|
||||
if (/^[A-Za-z0-9_.:-]{1,40}$/.test(k)) net[k] = series.valid(stored.net[k], 3, k === 'uplink' ? HIST_FAST : HIST_MAX, now, model.MAX_BPS);
|
||||
/* 'uplink' (before #88) belongs to an interface nobody noted */
|
||||
if (/^[A-Za-z0-9_.:-]{1,40}$/.test(k) && k !== 'uplink') net[k] = series.valid(stored.net[k], 3, histMax(k), now, model.MAX_BPS);
|
||||
});
|
||||
var clientHist = Object.create(null);
|
||||
if (stored.clients && typeof stored.clients === 'object')
|
||||
@@ -757,7 +772,7 @@ return view.extend({
|
||||
var m = model.build({
|
||||
board: this.board, info: raw.info, ifaces: raw.ifaces, devs: raw.devs, wifi: raw.wifi, iwinfo: raw.iwinfo,
|
||||
assoc: raw.assoc, hapd: raw.hapd, hapdStatus: raw.hapdStatus, hints: raw.hints, fdb: raw.fdb,
|
||||
switches: raw.switches, switchState: raw.switchState,
|
||||
switches: raw.switches, switchState: raw.switchState, routes: raw.routes,
|
||||
aliases: this.aliases, rdns: st.rdns, mdns: st.mdns,
|
||||
vendor: st.oui ? function(k) { return k ? st.oui.lookup(k) : null; } : null
|
||||
});
|
||||
@@ -790,7 +805,16 @@ return view.extend({
|
||||
st.uplinkRate = upRate || null;
|
||||
|
||||
/* throughput history (only for ticks that produced a rate) */
|
||||
if (upRate && !upFast) this.pushNet('uplink', at, upRate);
|
||||
if (upRate && !upFast) this.pushNet(lineKey(m.uplink), at, upRate);
|
||||
/* the other lines to the internet: this poll's (the fast lane
|
||||
samples the main one only) */
|
||||
this.extraLines(m).forEach(function(x) { var r = st.devRates[x.statsDev]; if (r) self.pushNet(lineKey(x), at, r); });
|
||||
/* a line gone for longer than the chart shows leaves no history */
|
||||
var lineKeys = m.uplinks.map(lineKey);
|
||||
Object.keys(st.hist.net).forEach(function(k) {
|
||||
var h = st.hist.net[k];
|
||||
if (/^up:/.test(k) && lineKeys.indexOf(k) < 0 && !(h.length && h[h.length - 1][0] > at - WINDOW_MS)) delete st.hist.net[k];
|
||||
});
|
||||
m.radios.forEach(function(r) { if (st.radioRates[r.id]) self.pushNet(r.id, at, st.radioRates[r.id]); });
|
||||
|
||||
/* station counters, per-station history */
|
||||
@@ -863,7 +887,7 @@ return view.extend({
|
||||
|
||||
pushNet: function(key, at, r) {
|
||||
var h = this.st.hist.net[key] || (this.st.hist.net[key] = []);
|
||||
series.push(h, [ at, r.rx, r.tx ], WINDOW_MS, key === 'uplink' ? HIST_FAST : HIST_MAX);
|
||||
series.push(h, [ at, r.rx, r.tx ], WINDOW_MS, histMax(key));
|
||||
},
|
||||
|
||||
/* ------------------------------------------------------- fast lane */
|
||||
@@ -901,7 +925,7 @@ return view.extend({
|
||||
if (dev && s) counters[dev] = { rx: s.rx_bytes, tx: s.tx_bytes };
|
||||
var dr = model.rates(st.fastPrev, counters, now);
|
||||
st.fastPrev = dr.next;
|
||||
if (dev && dr.rates[dev]) { st.uplinkRate = dr.rates[dev]; self.pushNet('uplink', at, st.uplinkRate); changed = true; }
|
||||
if (dev && dr.rates[dev]) { st.uplinkRate = dr.rates[dev]; self.pushNet(lineKey(up), at, st.uplinkRate); changed = true; }
|
||||
/* both answered: the full poll leaves CPU and the uplink to this lane */
|
||||
if (stat && (!dev || s)) st.fastAt = now;
|
||||
if (changed) self.paintFast();
|
||||
@@ -972,7 +996,7 @@ return view.extend({
|
||||
var m = model.build({
|
||||
board: this.board, info: raw.info, ifaces: raw.ifaces, devs: raw.devs, wifi: raw.wifi, iwinfo: raw.iwinfo,
|
||||
assoc: raw.assoc, hapd: raw.hapd, hapdStatus: raw.hapdStatus, hints: raw.hints, fdb: raw.fdb,
|
||||
switches: raw.switches, switchState: raw.switchState,
|
||||
switches: raw.switches, switchState: raw.switchState, routes: raw.routes,
|
||||
aliases: this.aliases, rdns: st.rdns, mdns: st.mdns,
|
||||
vendor: st.oui ? function(k) { return k ? st.oui.lookup(k) : null; } : null
|
||||
});
|
||||
@@ -1608,7 +1632,7 @@ return view.extend({
|
||||
the card's bottom edge; CPU scaled to at least 20 % so a quiet
|
||||
device still shows its shape */
|
||||
case 'uplink':
|
||||
return [ sparkline(st.hist.net.uplink || [], [ 1 ], 0, 'vt-spark-up', { min: 1000, quiet: true, bare: true }) ];
|
||||
return [ sparkline((this.m && this.m.uplink && st.hist.net[lineKey(this.m.uplink)]) || [], [ 1 ], 0, 'vt-spark-up', { min: 1000, quiet: true, bare: true }) ];
|
||||
case 'cpu':
|
||||
return [ sparkline(st.hist.cpu || [], [ 1 ], 0, 'vt-spark-cpu', { min: 20, quiet: true, bare: true }) ];
|
||||
case 'memory':
|
||||
@@ -1698,26 +1722,38 @@ return view.extend({
|
||||
/* the nodes the path shows: rebuilt only when this changes (a radio,
|
||||
SSID or network comes or goes); a poll updates text in place */
|
||||
heroKey: function() {
|
||||
var m = this.m;
|
||||
if (m.wired) return 'w\n' + m.networks.filter(function(n) { return n.up; }).slice(0, 8).map(function(n) { return n.name; }).join('\n');
|
||||
var m = this.m, lines = '\nlines=' + this.extraLines().map(function(x) { return x.iface; }).join(',');
|
||||
if (m.wired) return 'w\n' + m.networks.filter(function(n) { return n.up; }).slice(0, 8).map(function(n) { return n.name; }).join('\n') + lines;
|
||||
return 'r\n' + m.radios.map(function(r) {
|
||||
return r.id + '=' + m.ssids.filter(function(s) { return s.radio === r.id; }).map(function(s) { return s.id; }).join(',');
|
||||
}).join('\n') + '\nwired=' + this.wiredPorts().join(',');
|
||||
}).join('\n') + '\nwired=' + this.wiredPorts().join(',') + lines;
|
||||
},
|
||||
|
||||
/* the lines to the internet beside the main one (failover, mwan3;
|
||||
at most model.MAX_LINES) */
|
||||
extraLines: function(m) {
|
||||
m = m || this.m;
|
||||
return ((m && m.uplinks) || []).slice(1);
|
||||
},
|
||||
|
||||
/* the ports with wired hosts downstream of this device, at most 8:
|
||||
hosts on the uplink's port are the way to the gateway, not clients
|
||||
hosts on a line's port are the way to its gateway, not clients
|
||||
(on a bridge with several ports whose gateway port is unknown, only
|
||||
the gateway is left out) */
|
||||
wiredPorts: function() {
|
||||
var m = this.m, up = m.uplink, out = [];
|
||||
var m = this.m, out = [], self = this;
|
||||
(m.wiredHosts || []).forEach(function(c) {
|
||||
if (c.gateway || (up && up.port && c.port === up.port)) return;
|
||||
if (c.gateway || self.linePort(c.port)) return;
|
||||
if (out.indexOf(c.port) < 0) out.push(c.port);
|
||||
});
|
||||
return out.sort().slice(0, 8);
|
||||
},
|
||||
|
||||
/* the port is one a line to the internet leaves through */
|
||||
linePort: function(port) {
|
||||
return ((this.m && this.m.uplinks) || []).some(function(x) { return x.port && x.port === port; });
|
||||
},
|
||||
|
||||
/* the Clients tile on its wired list, on one port's hosts or all */
|
||||
showWired: function(port) {
|
||||
this.ui.kind = 'wired';
|
||||
@@ -1759,18 +1795,40 @@ return view.extend({
|
||||
if (c !== self.el.wires && c !== self.el.flows) path.removeChild(c);
|
||||
});
|
||||
|
||||
h.gwKicker = span('vt-node-kicker'); h.gwTitle = span('vt-node-title'); h.gwSub = span('vt-node-sub');
|
||||
h.gw = E('button', { 'type': 'button', 'class': 'vt-node vt-node-gw', 'click': function() { self.openDrawer('uplink', 'uplink'); } }, [
|
||||
/* more than one line to the internet: each by its interface, the
|
||||
main one (the default route the kernel takes) marked */
|
||||
var extra = this.extraLines();
|
||||
h.gwKicker = span(extra.length ? 'vt-node-name' : 'vt-node-kicker'); h.gwTitle = span('vt-node-title'); h.gwSub = span('vt-node-sub');
|
||||
h.gwTag = extra.length ? E('span', { 'class': 'vt-node-tag' }, [ _('default route') ]) : null;
|
||||
h.gw = E('button', { 'type': 'button', 'class': 'vt-node vt-node-gw', 'click': function() { self.openDrawer('uplink', ''); } }, [
|
||||
E('span', { 'class': 'vt-node-ico' }, [ icon('globe') ]),
|
||||
E('span', { 'class': 'vt-node-body' }, [ h.gwKicker, h.gwTitle, h.gwSub ])
|
||||
E('span', { 'class': 'vt-node-body' }, [
|
||||
h.gwTag ? E('span', { 'class': 'vt-node-kicker vt-node-kicker-tag' }, [ h.gwKicker, h.gwTag ]) : h.gwKicker,
|
||||
h.gwTitle, h.gwSub
|
||||
])
|
||||
]);
|
||||
h.lines = extra.map(function(x) {
|
||||
/* its rates as two items, so a narrow node wraps them rather than cut them */
|
||||
var ln = { id: x.iface, kicker: span('vt-node-name'), title: span('vt-node-title'), dn: E('span'), up: E('span') };
|
||||
ln.el = E('button', { 'type': 'button', 'class': 'vt-node vt-node-gw vt-node-line', 'click': function() { self.openDrawer('uplink', ln.id); } }, [
|
||||
E('span', { 'class': 'vt-node-ico' }, [ icon('globe') ]),
|
||||
E('span', { 'class': 'vt-node-body' }, [ E('span', { 'class': 'vt-node-kicker' }, [ ln.kicker ]), ln.title, E('span', { 'class': 'vt-node-sub vt-node-rates vt-tn' }, [ ln.dn, ln.up ]) ])
|
||||
]);
|
||||
return ln;
|
||||
});
|
||||
h.linesEl = extra.length ? E('div', { 'class': 'vt-lines' }, h.lines.map(function(ln) { return ln.el; })) : null;
|
||||
setClass(path, 'vt-path' + (extra.length ? ' vt-path-lines' : ''));
|
||||
|
||||
h.dnVal = E('b'); h.dnUnit = E('small'); h.upVal = E('b'); h.upUnit = E('small'); h.linkMeta = span('vt-link-meta');
|
||||
/* a full-tunnel VPN over the line: named on the link it runs through */
|
||||
h.tunName = span('vt-link-tunnel-name'); h.tunProto = span('vt-link-tunnel-proto');
|
||||
h.tunnel = E('span', { 'class': 'vt-link-tunnel', 'hidden': '' }, [ icon('lock'), h.tunName, h.tunProto ]);
|
||||
h.link = E('div', { 'class': 'vt-link-label' }, [
|
||||
E('span', { 'class': 'vt-link-rate vt-link-down' }, [ icon('down'), h.dnVal, h.dnUnit ]),
|
||||
E('span', { 'class': 'vt-link-below' }, [
|
||||
E('span', { 'class': 'vt-link-rate vt-link-up' }, [ icon('up'), h.upVal, h.upUnit ]),
|
||||
h.linkMeta
|
||||
h.linkMeta,
|
||||
h.tunnel
|
||||
])
|
||||
]);
|
||||
|
||||
@@ -1782,6 +1840,7 @@ return view.extend({
|
||||
|
||||
h.branches = E('div', { 'class': 'vt-branches' });
|
||||
h.watch = [ path, h.gw, h.link, h.ap, h.branches ];
|
||||
h.lines.forEach(function(ln) { h.watch.push(ln.el); });
|
||||
if (m.wired) {
|
||||
m.networks.filter(function(n) { return n.up; }).slice(0, 8).forEach(function(n) {
|
||||
var b = { id: n.name, ssids: [], kicker: span('vt-node-kicker'), title: span('vt-node-title'), addr: span('vt-node-sub vt-mono'), sub: span('vt-node-sub vt-tn') };
|
||||
@@ -1844,6 +1903,8 @@ return view.extend({
|
||||
|
||||
path.insertBefore(h.gw, this.el.wires);
|
||||
path.insertBefore(h.link, this.el.wires);
|
||||
/* after the main line's rates: on a phone they stay with it */
|
||||
if (h.linesEl) path.insertBefore(h.linesEl, this.el.wires);
|
||||
path.insertBefore(h.ap, this.el.wires);
|
||||
path.insertBefore(h.branches, this.el.wires);
|
||||
this.hero = h;
|
||||
@@ -1861,7 +1922,7 @@ return view.extend({
|
||||
name once one is known beyond its vendor */
|
||||
var gwHost = (m.wiredHosts || []).filter(function(c) { return c.gateway; })[0];
|
||||
var gwName = gwHost && /^(alias|dhcp|mdns|dns)$/.test(gwHost.nameSource) ? gwHost.name : null;
|
||||
txt(h.gwKicker, up && up.isWan ? _('Internet') : _('Gateway'));
|
||||
txt(h.gwKicker, h.lines.length && up ? up.iface : up && up.isWan ? _('Internet') : _('Gateway'));
|
||||
txt(h.gwTitle, gwName || (up && up.gateway ? up.gateway : up && up.defaultRoute ? up.l3dev : _('No default route', 'vantage')));
|
||||
txt(h.gwSub, gwName ? up.gateway : up && up.dns.length ? _('DNS %s').format(up.dns[0]) : _('Reachability not probed'));
|
||||
|
||||
@@ -1870,6 +1931,13 @@ return view.extend({
|
||||
txt(h.upVal, upR ? fmt.bitsParts(upR.tx).v : fmt.DASH);
|
||||
txt(h.upUnit, upR ? fmt.bitsParts(upR.tx).u : '');
|
||||
txt(h.linkMeta, up ? [ up.dev, up.speed ? (up.speed.mbps >= 1000 ? (up.speed.mbps / 1000) + 'G' : up.speed.mbps + 'M') + (up.speed.duplex === 'full' ? ' FD' : up.speed.duplex === 'half' ? ' HD' : '') : null ].filter(Boolean).join(' · ') : _('no uplink'));
|
||||
var tun = up && up.tunnel, tunR = tun ? st.devRates[tun.dev] : null;
|
||||
if (h.tunnel.hidden !== !tun) h.tunnel.hidden = !tun;
|
||||
txt(h.tunName, tun ? tun.iface : '');
|
||||
txt(h.tunProto, tun && tun.proto ? tun.proto : '');
|
||||
setTitle(h.tunnel, tun ? _('Traffic to the internet goes through %s, a tunnel over this line').format(tun.iface) +
|
||||
(tunR ? ' · ↓ ' + fmt.bits(tunR.rx) + ' · ↑ ' + fmt.bits(tunR.tx) : '') : '');
|
||||
if (h.gwTag) txt(h.gwTag, tun ? _('default route via %s').format(tun.iface) : _('default route'));
|
||||
|
||||
txt(h.apKicker, m.wired ? _('This device') : _('This access point'));
|
||||
txt(h.apTitle, d.model);
|
||||
@@ -1879,6 +1947,22 @@ return view.extend({
|
||||
h.upRate = upR ? upR.rx + upR.tx : 0;
|
||||
if (this.backdrop) this.backdrop.update({ down: upR ? upR.rx : null, up: upR ? upR.tx : null, origin: h.ap });
|
||||
|
||||
var lines = Object.create(null);
|
||||
this.extraLines().forEach(function(x) { lines[x.iface] = x; });
|
||||
h.lines.forEach(function(ln) {
|
||||
var x = lines[ln.id], r = x ? st.devRates[x.statsDev] : null;
|
||||
if (!x) return;
|
||||
setTitle(ln.el, [ x.iface, x.dev, x.speed ? model.speedLabel(x.speed) : null ].filter(Boolean).join(' · '));
|
||||
setClass(ln.el, 'vt-node vt-node-gw vt-node-line' + (x.carrier === false ? ' vt-node-off' : ''));
|
||||
txt(ln.kicker, x.iface);
|
||||
txt(ln.title, x.gateway || x.l3dev);
|
||||
var p = r && !(r.rx === 0 && r.tx === 0) ? fmt.rateParts(r.rx, r.tx) : null;
|
||||
txt(ln.dn, p ? '↓ ' + p.dn : r ? _('idle') : fmt.DASH);
|
||||
txt(ln.up, p ? '↑ ' + p.up + (p.unit ? ' ' + p.unit : '') : '');
|
||||
ln.active = !!(r && r.rx + r.tx > 50e3);
|
||||
ln.rate = r ? r.rx + r.tx : 0;
|
||||
});
|
||||
|
||||
if (m.wired) {
|
||||
var nets = Object.create(null);
|
||||
m.networks.forEach(function(n) { if (n.up && !nets[n.name]) nets[n.name] = n; });
|
||||
@@ -1976,7 +2060,8 @@ return view.extend({
|
||||
wireList: function() {
|
||||
var h = this.hero, out = [];
|
||||
if (!h) return out;
|
||||
out.push({ key: 'gw', from: h.gw, to: h.ap, active: h.upActive, speed: wires.speed(h.upRate), main: true });
|
||||
out.push({ key: 'gw', from: h.gw, to: h.ap, active: h.upActive, speed: wires.speed(h.upRate), main: true, tunnel: !!(this.m && this.m.uplink && this.m.uplink.tunnel) });
|
||||
h.lines.forEach(function(ln, k) { out.push({ key: 'l:' + ln.id, from: ln.el, to: h.ap, active: ln.active, speed: wires.speed(ln.rate), line: k }); });
|
||||
h.radios.forEach(function(b) {
|
||||
out.push({ key: 'r:' + b.id, from: h.ap, to: b.el, active: b.active, speed: wires.speed(b.rate) });
|
||||
b.ssids.forEach(function(sn) { out.push({ key: 's:' + b.id + ':' + sn.id, from: b.el, to: sn.el, active: sn.active, speed: 1 }); });
|
||||
@@ -2023,7 +2108,7 @@ return view.extend({
|
||||
svg.appendChild(g.flow);
|
||||
self.el.flows.appendChild(g.fw);
|
||||
}
|
||||
var c = wires.curve(rect(w.from), rect(w.to));
|
||||
var c = w.line != null ? wires.line(rect(w.from), rect(w.to), w.line) : wires.curve(rect(w.from), rect(w.to));
|
||||
if (g.d !== c.d) {
|
||||
g.base.setAttribute('d', c.d);
|
||||
g.flow.setAttribute('d', c.d);
|
||||
@@ -2052,7 +2137,7 @@ return view.extend({
|
||||
this.wireList().forEach(function(w) {
|
||||
var g = map[w.key];
|
||||
if (!g) return;
|
||||
setClass(g.base, 'vt-wire' + (w.main ? ' vt-wire-main' : '') + (w.active ? ' vt-wire-on' : ''));
|
||||
setClass(g.base, 'vt-wire' + (w.main ? ' vt-wire-main' : '') + (w.tunnel ? ' vt-wire-tunnel' : '') + (w.active ? ' vt-wire-on' : ''));
|
||||
setClass(g.flow, 'vt-flow vt-flow-' + w.speed + (w.active ? ' vt-flow-on' : ''));
|
||||
self.syncFlow(g, !!(w.active && !still && g.d), w.speed);
|
||||
});
|
||||
@@ -2253,7 +2338,8 @@ return view.extend({
|
||||
]));
|
||||
},
|
||||
|
||||
/* one small multiple per link: the uplink, then each radio, each with
|
||||
/* one small multiple per link: the uplink, the other lines to the
|
||||
internet, then each radio, each with
|
||||
its own y scale (a busy uplink would flatten an idle radio), on a
|
||||
shared time axis and crosshair. Download (towards the clients) and
|
||||
upload each keep their direction's colour; how they share the plot
|
||||
@@ -2262,7 +2348,8 @@ return view.extend({
|
||||
download, a radio sends it. */
|
||||
chartSeries: function() {
|
||||
var m = this.m;
|
||||
var list = [ { key: 'uplink', label: m.uplink ? _('Uplink %s').format(m.uplink.dev) : _('Uplink', 'vantage'), dn: 1, up: 2 } ];
|
||||
var list = [ { key: m.uplink ? lineKey(m.uplink) : 'uplink', label: m.uplink ? _('Uplink %s').format(m.uplink.dev) : _('Uplink', 'vantage'), dn: 1, up: 2 } ];
|
||||
this.extraLines().forEach(function(x) { list.push({ key: lineKey(x), label: _('Uplink %s').format(x.dev), dn: 1, up: 2 }); });
|
||||
m.radios.forEach(function(r) { list.push({ key: r.id, label: _('%s radio').format(r.bandLabel), dn: 2, up: 1 }); });
|
||||
return list;
|
||||
},
|
||||
@@ -2278,7 +2365,10 @@ return view.extend({
|
||||
start part way across and the minute labels stay where they are
|
||||
(a span that grew with the history moved them every poll, #71) */
|
||||
var dm = series.domain(list.map(function(s) { return net[s.key]; }), now, WINDOW_MS, WINDOW_MS);
|
||||
dm.full = dm.age >= WINDOW_MS - 1000;
|
||||
/* history keeps only samples newer than the window, so its oldest is
|
||||
up to one poll short of five minutes: allow two polls, or the
|
||||
subtitle flips to "collecting" with a full history */
|
||||
dm.full = dm.age >= WINDOW_MS - POLL_S * 2000;
|
||||
var ticks = [];
|
||||
for (var k = 1; k * 60000 < dm.span - 20000; k++) ticks.push(k);
|
||||
var rows = [], samples = 0;
|
||||
@@ -2857,9 +2947,8 @@ return view.extend({
|
||||
var bar = E('span', { 'class': 'vt-mini-fill' });
|
||||
setW(bar, maxRate > 0 ? total / maxRate * 100 : 0);
|
||||
var tags = [];
|
||||
var up = this.m && this.m.uplink;
|
||||
if (c.gateway) tags.push(pill(_('Gateway'), 'accent', _('The router this device sends its traffic to')));
|
||||
else if (up && up.port && c.port === up.port) tags.push(pill(_('Upstream'), 'muted', _('Reached through the uplink port %s, not connected to this device').format(c.port)));
|
||||
else if (this.linePort(c.port)) tags.push(pill(_('Upstream'), 'muted', _('Reached through the uplink port %s, not connected to this device').format(c.port)));
|
||||
if (c.vlan != null) tags.push(pill(_('VLAN %d').format(c.vlan), 'muted'));
|
||||
var speed = c.speed ? fmt.phyRate(c.speed.mbps * 1000) : fmt.DASH;
|
||||
var link = E('td', { 'class': 'vt-td-link', 'data-title': _('Link', 'vantage') }, [
|
||||
@@ -3033,7 +3122,7 @@ return view.extend({
|
||||
this.ui.editing = null;
|
||||
/* the address names what is shown, also when the open drawer
|
||||
moves on (from a radio to one of its clients) */
|
||||
try { history.replaceState(null, '', '#' + kind + '=' + encodeURIComponent(id)); } catch (e) {}
|
||||
try { history.replaceState(null, '', '#' + kind + (kind === 'uplink' && !id ? '' : '=' + encodeURIComponent(id))); } catch (e) {}
|
||||
if (!el.drawer.hidden) { this.paintDrawer(); this.paintClients(); return; }
|
||||
this.lastFocus = document.activeElement;
|
||||
el.drawer.hidden = false;
|
||||
@@ -3056,16 +3145,19 @@ return view.extend({
|
||||
},
|
||||
|
||||
/* A deep link names an existing thing or nothing: a client by a
|
||||
station MAC, a radio this device reports; uplink and device take no
|
||||
id. Anything else is dropped from the address bar, so a crafted link
|
||||
cannot put its own text in the drawer title. */
|
||||
station MAC, a radio this device reports, a line to the internet by
|
||||
its interface (#uplink alone: the main one, whichever it is); the
|
||||
device takes no id. Anything else is dropped from the address bar,
|
||||
so a crafted link cannot put its own text in the drawer title. */
|
||||
openFromHash: function() {
|
||||
var hash = location.hash || '', m = /^#(client|radio|uplink|device)=(.{1,128})$/.exec(hash), id = null;
|
||||
if (hash === '#uplink') { this.openDrawer('uplink', ''); return; }
|
||||
if (m) {
|
||||
try { id = decodeURIComponent(m[2]); } catch (e) { id = null; }
|
||||
if (id == null) { /* malformed escape */ }
|
||||
else if (m[1] === 'client') id = names.normMac(id);
|
||||
else if (m[1] === 'radio') id = this.m && this.m.radios.some(function(r) { return r.id === id; }) ? id : null;
|
||||
else if (m[1] === 'uplink') id = ((this.m && this.m.uplinks) || []).some(function(x) { return x.iface === id; }) ? id : '';
|
||||
else id = m[1];
|
||||
}
|
||||
if (id != null) { this.openDrawer(m[1], id); return; }
|
||||
@@ -3081,7 +3173,7 @@ return view.extend({
|
||||
var content;
|
||||
if (d.kind === 'client') content = this.drawerClient(d.id);
|
||||
else if (d.kind === 'radio') content = this.drawerRadio(d.id);
|
||||
else if (d.kind === 'uplink') content = this.drawerUplink();
|
||||
else if (d.kind === 'uplink') content = this.drawerUplink(d.id);
|
||||
else content = this.drawerDevice();
|
||||
dom.content(el.drawerTitle, content.title);
|
||||
dom.content(el.drawerBody, content.body);
|
||||
@@ -3368,9 +3460,26 @@ return view.extend({
|
||||
};
|
||||
},
|
||||
|
||||
drawerUplink: function() {
|
||||
var up = this.m.uplink, r = this.st.uplinkRate;
|
||||
/* the main line (id ''), whichever interface it is, or one line by
|
||||
its interface: still that one after a failover makes it the main
|
||||
line, and said to be down when it is no line any more (never
|
||||
another interface under its name) */
|
||||
drawerUplink: function(id) {
|
||||
var m = this.m, st = this.st, lines = m.uplinks || [];
|
||||
var up = id ? lines.filter(function(x) { return x.iface === id; })[0] : m.uplink;
|
||||
var r = up && up === m.uplink ? st.uplinkRate : up ? st.devRates[up.statsDev] || null : null;
|
||||
var hist = up ? st.hist.net[lineKey(up)] : null, several = lines.length > 1;
|
||||
var tunR = up && up.tunnel ? st.devRates[up.tunnel.dev] || null : null;
|
||||
function errors(n) { return fmt.N_(n, '%s error', '%s errors').format(fmt.count(n)); }
|
||||
if (id && !up) return {
|
||||
title: [ E('span', { 'class': 'vt-drawer-ico' }, [ icon('globe') ]), E('div', { 'class': 'vt-drawer-titles' }, [
|
||||
E('div', { 'class': 'vt-drawer-name' }, [ _('Uplink · %s').format(id) ])
|
||||
]) ],
|
||||
body: [
|
||||
E('div', { 'class': 'vt-dactions' }, [ pageLink(insight.PAGES.network, _('Interfaces')) ]),
|
||||
E('p', { 'class': 'vt-empty-inline' }, [ _('This line is down or has no default route now.') ])
|
||||
]
|
||||
};
|
||||
if (!up) return { title: [ E('span', { 'class': 'vt-drawer-name' }, [ _('Uplink', 'vantage') ]) ], body: [ E('p', { 'class': 'vt-empty-inline' }, [ _('No interface with a route is up.') ]) ] };
|
||||
var s = up.stats || {};
|
||||
return {
|
||||
@@ -3384,17 +3493,22 @@ return view.extend({
|
||||
E('div', { 'class': 'vt-dcard' }, [ E('div', { 'class': 'vt-dcard-label' }, [ _('From network') ]), E('div', { 'class': 'vt-dcard-value' }, [ r ? fmt.bits(r.rx) : fmt.DASH ]), E('div', { 'class': 'vt-dcard-sub' }, [ _('received on %s').format(up.dev) ]) ]),
|
||||
E('div', { 'class': 'vt-dcard' }, [ E('div', { 'class': 'vt-dcard-label' }, [ _('To network') ]), E('div', { 'class': 'vt-dcard-value' }, [ r ? fmt.bits(r.tx) : fmt.DASH ]), E('div', { 'class': 'vt-dcard-sub' }, [ _('sent on %s').format(up.dev) ]) ])
|
||||
]),
|
||||
E('div', { 'class': 'vt-dspark' }, [ sparkline(this.st.hist.net.uplink || [], [ 1, 2 ], 0, 'vt-spark-up', { min: 1000, scale: bitsLabel }) ]),
|
||||
E('div', { 'class': 'vt-dspark' }, [ sparkline(hist || [], [ 1, 2 ], 0, 'vt-spark-up', { min: 1000, scale: bitsLabel }) ]),
|
||||
this.section(_('Link', 'vantage'), [ E('dl', { 'class': 'vt-kvs' }, [
|
||||
kv(_('Carrier'), up.carrier === false ? _('No link') : _('Up', 'vantage')),
|
||||
kv(_('Speed', 'vantage'), model.speedLabel(up.speed)),
|
||||
kv(_('Protocol'), up.proto || fmt.DASH),
|
||||
kv(_('Interface up for'), up.uptime != null ? fmt.duration(up.uptime) : fmt.DASH)
|
||||
kv(_('Interface up for'), up.uptime != null ? fmt.duration(up.uptime) : fmt.DASH),
|
||||
/* a full-tunnel VPN over this line: ordinary traffic goes through it */
|
||||
up.tunnel ? kv(_('Tunnel'), up.tunnel.iface + (up.tunnel.proto ? ' · ' + up.tunnel.proto : '')) : '',
|
||||
up.tunnel ? kv(_('In the tunnel'), tunR ? '↓ ' + fmt.bits(tunR.rx) + ' · ↑ ' + fmt.bits(tunR.tx) : fmt.DASH) : ''
|
||||
]) ]),
|
||||
this.section(_('Addresses'), [ E('dl', { 'class': 'vt-kvs' }, [
|
||||
kv(_('IPv4'), up.ipv4.join('\n') || fmt.DASH, true),
|
||||
kv(_('IPv6'), up.ipv6.join('\n') || fmt.DASH, true),
|
||||
kv(_('Gateway'), up.gateway || fmt.DASH, true),
|
||||
/* beside other lines, the metric says which one the kernel takes */
|
||||
several ? kv(_('Route metric'), up.metric != null ? String(up.metric) : fmt.DASH, true) : '',
|
||||
kv(_('DNS'), up.dns.join('\n') || fmt.DASH, true)
|
||||
]) ]),
|
||||
this.section(_('Counters since boot'), [ E('dl', { 'class': 'vt-kvs' }, [
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"luci-app-vantage": {
|
||||
"description": "Vantage dashboard: read-only status of the device, uplink, radios and stations (wireless configuration without keys, through luci.vantage wireless), its temperature sensors (luci.vantage thermal), the wired hosts its bridges have learned (luci.vantage wired: MAC, port and when last heard, and which switch ports a port's VLAN has) and the link state of those switch ports (luci getSwconfigPortState)",
|
||||
"description": "Vantage dashboard: read-only status of the device, uplink, radios and stations (wireless configuration without keys, through luci.vantage wireless), its temperature sensors (luci.vantage thermal), the wired hosts its bridges have learned (luci.vantage wired: MAC, port and when last heard, and which switch ports a port's VLAN has), the link state of those switch ports (luci getSwconfigPortState) and the default route the kernel takes to the internet (luci.vantage routes: family, device and metric, no gateways)",
|
||||
"read": {
|
||||
"file": {
|
||||
"/proc/stat": [ "read" ]
|
||||
@@ -12,7 +12,7 @@
|
||||
"network.device": [ "status" ],
|
||||
"luci-rpc": [ "getHostHints" ],
|
||||
"luci": [ "getSwconfigPortState" ],
|
||||
"luci.vantage": [ "thermal", "wired", "wireless" ],
|
||||
"luci.vantage": [ "routes", "thermal", "wired", "wireless" ],
|
||||
"iwinfo": [ "assoclist", "info" ],
|
||||
"hostapd.*": [ "get_clients", "get_status" ],
|
||||
"umdns": [ "hosts" ],
|
||||
|
||||
@@ -26,6 +26,18 @@
|
||||
// /etc/config/network (device, vlan, vid and ports, nothing
|
||||
// else), or board.json's roles for a switch those give no
|
||||
// VLAN. Read group luci-app-vantage.
|
||||
// routes the route the kernel takes to the internet, IPv4 and IPv6:
|
||||
// a read-only lookup of each family (as `ip route get
|
||||
// fibmatch`) of a documentation address no network uses, so
|
||||
// the answer is the default route ordinary traffic takes.
|
||||
// For each family whose answer is a unicast default route in
|
||||
// the main table: its family (4 or 6), the device it leaves by
|
||||
// (null over several next hops) and its metric; no gateways.
|
||||
// netifd lists the routes it asked for; of two with the same
|
||||
// metric the kernel keeps the one added last. routes is null
|
||||
// when a lookup could not be sent. One lookup a family, so
|
||||
// the work does not grow with the routing table. Read group
|
||||
// luci-app-vantage.
|
||||
// set_alias { mac, name, icon }: name one station, or remove its name
|
||||
// with name ''. Validated here, whatever the browser did, and
|
||||
// written to /etc/config/vantage only. Write group
|
||||
@@ -52,6 +64,8 @@ const THERMAL = '/sys/class/thermal';
|
||||
const MAX_ZONES = 16;
|
||||
const MAX_HOSTS = 512; // wired hosts per reply
|
||||
const MAX_PER_PORT = 128; // of them on one port (the uplink's would take them all)
|
||||
const PROBE4 = '198.51.100.1'; // routes: looked up, never sent to (documentation ranges)
|
||||
const PROBE6 = '2001:db8::1';
|
||||
const NET = '/sys/class/net';
|
||||
const FDB_LIMIT = 4096; // forwarding entries, all bridges together, before wired() lists none
|
||||
const FDB_ENTRY = 16; // sizeof(struct __fdb_entry), brforward's record
|
||||
@@ -478,6 +492,31 @@ function wired() {
|
||||
return out;
|
||||
}
|
||||
|
||||
// [ { family, dev, metric } ] for the families whose lookup answers with
|
||||
// a unicast default route in the main table (no dst), IPv4 then IPv6; dev
|
||||
// null over several next hops (multipath, no single oif). null when a
|
||||
// lookup could not be sent; false is the kernel's error, for a lookup that
|
||||
// there is no route (ENETUNREACH), so that family is left out.
|
||||
function routes() {
|
||||
let out = [];
|
||||
|
||||
for (let q in [ { family: rtnl.const.AF_INET, dst: PROBE4 }, { family: rtnl.const.AF_INET6, dst: PROBE6 } ]) {
|
||||
let r = rtnl.request(rtnl.const.RTM_GETROUTE, 0, { family: q.family, dst: q.dst, flags: rtnl.const.RTM_F_FIB_MATCH });
|
||||
|
||||
if (r === null)
|
||||
return null;
|
||||
|
||||
if (type(r) != 'object' || r.family != q.family || r.dst != null || r.table != rtnl.const.RT_TABLE_MAIN || r.type != rtnl.const.RTN_UNICAST)
|
||||
continue;
|
||||
|
||||
let m = r.priority ?? 0;
|
||||
|
||||
push(out, { family: (q.family == rtnl.const.AF_INET) ? 4 : 6, dev: netdev(r.oif), metric: (type(m) == 'int' && m >= 0) ? m : 0 });
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// a switch port's number (0..63), or null
|
||||
function switch_port(v) {
|
||||
return (type(v) == 'int' && v >= 0 && v < 64) ? v : null;
|
||||
@@ -631,6 +670,12 @@ const methods = {
|
||||
}
|
||||
},
|
||||
|
||||
routes: {
|
||||
call: function() {
|
||||
return { routes: routes() };
|
||||
}
|
||||
},
|
||||
|
||||
set_alias: {
|
||||
args: { mac: 'mac', name: 'name', icon: 'icon' },
|
||||
call: function(request) {
|
||||
|
||||
@@ -11,7 +11,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-theme-vantage
|
||||
PKG_VERSION:=1.0.15
|
||||
PKG_RELEASE:=32
|
||||
PKG_RELEASE:=33
|
||||
PKG_LICENSE:=GPL-3.0-or-later
|
||||
|
||||
LUCI_TITLE:=Vantage theme
|
||||
|
||||
@@ -2594,6 +2594,9 @@ html:not([data-darkmode="true"]) .vt-login-page::before {
|
||||
#packages {
|
||||
width: 100% !important;
|
||||
}
|
||||
vt-brand-word {
|
||||
margin:0;
|
||||
}
|
||||
.vt-login-card {
|
||||
padding: var(--sp-6);
|
||||
background: var(--s1);
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
<circle cx="21.5" cy="10.5" r="1.9" class="vt-brand-dot"></circle>
|
||||
<circle cx="21.5" cy="21.5" r="1.9" class="vt-brand-dot"></circle>
|
||||
</svg>
|
||||
<span class="vt-brand-word">{{ hostname }}</span>
|
||||
<h3 class="vt-brand-word">{{ hostname }}</h3>
|
||||
</a>
|
||||
<ul class="vt-rail-list" id="vt-rail-list"></ul>
|
||||
<div class="vt-rail-foot">
|
||||
|
||||
Reference in new issue
Block a user