gluon/package/gluon-status-page/javascript/status-page.js
Matthias Schiffer 0d3fa6b59b
gluon-status-page: use ubus to get radio channels
Do not depend on the respondd-airtime module just to get the configured
channels. This removes the display of the frequency in addition to the
channel, as it is not readily available.

In addition, the translation string is improved to allow for text after
the channel number.
2021-08-08 12:11:20 +02:00

823 lines
18 KiB
JavaScript

/*
Build using:
uglifyjs javascript/status-page.js -o files/lib/gluon/status-page/www/static/status-page.js -c -m
*/
'use strict';
(function() {
var _ = JSON.parse(document.body.getAttribute('data-translations'));
String.prototype.sprintf = function() {
var i = 0;
var args = arguments;
return this.replace(/%s/g, function() {
return args[i++];
});
};
function formatNumberFixed(d, digits) {
return d.toFixed(digits).replace(/\./, _['.'])
}
function formatNumber(d, digits) {
digits--;
for (var v = d; v >= 10 && digits > 0; v /= 10)
digits--;
// avoid toPrecision as it might produce strings in exponential notation
return formatNumberFixed(d, digits);
}
function prettyPackets(d) {
return _['%s packets/s'].sprintf(formatNumberFixed(d, 0));
}
function prettyPrefix(prefixes, step, d) {
var prefix = 0;
if (d === undefined)
return "- ";
while (d > step && prefix < prefixes.length - 1) {
d /= step;
prefix++;
}
d = formatNumber(d, 3);
return d + " " + prefixes[prefix];
}
function prettySize(d) {
return prettyPrefix([ "", "K", "M", "G", "T" ], 1024, d);
}
function prettyBits(d) {
return prettySize(8 * d) + "bps";
}
function prettyBytes(d) {
return prettySize(d) + "B";
}
var formats = {
'id': function(value) {
return value;
},
'decimal': function(value) {
return formatNumberFixed(value, 2);
},
'percent': function(value) {
return _['%s used'].sprintf(formatNumber(100 * value, 3) + '%');
},
'memory': function(memory) {
var usage = 1 - memory.available / memory.total
return formats.percent(usage);
},
'time': function(seconds) {
var minutes = Math.round(seconds / 60);
var days = Math.floor(minutes / 1440);
var hours = Math.floor((minutes % 1440) / 60);
minutes = Math.floor(minutes % 60);
var out = '';
if (days === 1)
out += _['1 day'] + ', ';
else if (days > 1)
out += _['%s days'].sprintf(days) + ", ";
out += hours + ":";
if (minutes < 10)
out += "0";
out += minutes;
return out;
},
'packetsDiff': function(packets, packetsPrev, diff) {
if (diff > 0)
return prettyPackets((packets-packetsPrev) / diff);
},
'bytesDiff': function(bytes, bytesPrev, diff) {
if (diff > 0)
return prettyBits((bytes-bytesPrev) / diff);
},
'bytes': function(bytes) {
return prettyBytes(bytes);
},
'neighbour': function(addr) {
if (!addr)
return '';
for (var i in interfaces) {
var iface = interfaces[i];
var neigh = iface.lookup_neigh(addr);
if (!neigh)
continue;
var span = document.createElement('span');
span.appendChild(document.createTextNode('via '));
var a = document.createElement('a');
a.href = 'http://[' + neigh.get_addr() + ']/';
a.textContent = neigh.get_hostname();
span.appendChild(a);
span.appendChild(document.createTextNode(' (' + i + ')'));
return span;
}
return 'via ' + addr + ' (unknown iface)';
}
}
function resolve_key(obj, key) {
key.split('/').forEach(function(part) {
if (obj)
obj = obj[part];
});
return obj;
}
function add_event_source(url, handler) {
var source = new EventSource(url);
var prev = {};
source.onmessage = function(m) {
var data = JSON.parse(m.data);
handler(data, prev);
prev = data;
}
source.onerror = function() {
source.close();
window.setTimeout(function() {
add_event_source(url, handler);
}, 3000);
}
}
var node_address = document.body.getAttribute('data-node-address');
var location;
try {
location = JSON.parse(document.body.getAttribute('data-node-location'));
} catch (e) {
}
function update_mesh_vpn(data) {
function add_group(peers, d) {
Object.keys(d.peers || {}).forEach(function(peer) {
peers.push([peer, d.peers[peer]]);
});
Object.keys(d.groups || {}).forEach(function(group) {
add_group(peers, d.groups[group]);
});
return peers;
}
var div = document.getElementById('mesh-vpn');
if (!data) {
div.style.display = 'none';
return;
}
div.style.display = '';
var table = document.getElementById('mesh-vpn-peers');
while (table.lastChild)
table.removeChild(table.lastChild);
var peers = add_group([], data);
peers.sort();
peers.forEach(function (peer) {
var tr = document.createElement('tr');
var th = document.createElement('th');
th.textContent = peer[0];
tr.appendChild(th);
var td = document.createElement('td');
if (peer[1])
td.textContent = _['connected'] + ' (' + formats.time(peer[1].established) + ')';
else
td.textContent = _['not connected'];
tr.appendChild(td);
table.appendChild(tr);
});
}
var statisticsElems = document.querySelectorAll('[data-statistics]');
add_event_source('/cgi-bin/dyn/statistics', function(data, dataPrev) {
var diff = data.uptime - dataPrev.uptime;
statisticsElems.forEach(function(elem) {
var stat = elem.getAttribute('data-statistics');
var format = elem.getAttribute('data-format');
var valuePrev = resolve_key(dataPrev, stat);
var value = resolve_key(data, stat);
try {
var format_result = formats[format](value, valuePrev, diff);
switch (typeof format_result) {
case "string":
elem.textContent = format_result;
break;
case "object":
if (elem.lastChild)
elem.removeChild(elem.lastChild);
elem.appendChild(format_result);
break;
}
} catch (e) {
console.error(e);
}
});
try {
update_mesh_vpn(data.mesh_vpn);
} catch (e) {
console.error(e);
}
})
function haversine(lat1, lon1, lat2, lon2) {
var rad = Math.PI / 180;
lat1 *= rad; lon1 *= rad; lat2 *= rad; lon2 *= rad;
var R = 6372.8; // km
var dLat = lat2 - lat1;
var dLon = lon2 - lon1;
var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2);
var c = 2 * Math.asin(Math.sqrt(a));
return R * c;
}
var interfaces = {};
function Signal(color) {
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
var value = null;
var radius = 1.2;
function drawPixel(x, y) {
ctx.beginPath();
ctx.fillStyle = color;
ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
ctx.closePath();
ctx.fill();
}
return {
'canvas': canvas,
'highlight': false,
'resize': function(w, h) {
var lastImage;
try {
ctx.getImageData(0, 0, w, h);
} catch (e) {}
canvas.width = w;
canvas.height = h;
if (lastImage)
ctx.putImageData(lastImage, 0, 0);
},
'draw': function(x, scale) {
var y = scale(value);
ctx.clearRect(x, 0, 5, canvas.height)
if (y)
drawPixel(x, y)
},
'set': function (d) {
value = d;
},
};
}
function SignalGraph() {
var min = -100, max = 0;
var i = 0;
var signals = [];
var canvas = document.createElement('canvas');
canvas.className = 'signalgraph';
canvas.height = 200;
var ctx = canvas.getContext('2d');
function scaleInverse(n, min, max, height) {
return (min * n + max * (height - n)) / height;
}
function scale(n, min, max, height) {
return (1 - (n - min) / (max - min)) * height;
}
function drawGrid() {
var nLines = Math.floor(canvas.height / 40);
ctx.save();
ctx.lineWidth = 0.5;
ctx.strokeStyle = 'rgba(0, 0, 0, 0.25)';
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.textAlign = 'end';
ctx.textBaseline = 'bottom';
ctx.beginPath();
for (var i = 0; i < nLines; i++) {
var y = canvas.height - i * 40;
ctx.moveTo(0, y - 0.5);
ctx.lineTo(canvas.width, y - 0.5);
var dBm = Math.round(scaleInverse(y, min, max, canvas.height)) + ' dBm';
ctx.save();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)';
ctx.lineWidth = 4;
ctx.miterLimit = 2;
ctx.strokeText(dBm, canvas.width - 5, y - 2.5);
ctx.fillText(dBm, canvas.width - 5, y - 2.5);
ctx.restore();
}
ctx.stroke();
ctx.strokeStyle = 'rgba(0, 0, 0, 0.83)';
ctx.lineWidth = 1.5;
ctx.strokeRect(0.5, 0.5, canvas.width - 1, canvas.height - 1);
ctx.restore();
}
function resize() {
canvas.width = canvas.clientWidth;
signals.forEach(function(signal) {
signal.resize(canvas.width, canvas.height);
});
}
resize();
function draw() {
if (canvas.clientWidth === 0)
return;
if (canvas.width !== canvas.clientWidth)
resize();
ctx.clearRect(0, 0, canvas.width, canvas.height);
var highlight = false;
signals.forEach(function(signal) {
if (signal.highlight)
highlight = true;
});
ctx.save();
signals.forEach(function(signal) {
if (highlight)
ctx.globalAlpha = 0.2;
if (signal.highlight)
ctx.globalAlpha = 1;
signal.draw(i, function(value) {
return scale(value, min, max, canvas.height);
});
ctx.drawImage(signal.canvas, 0, 0);
});
ctx.restore();
ctx.save();
ctx.beginPath();
ctx.strokeStyle = 'rgba(255, 180, 0, 0.15)';
ctx.lineWidth = 5;
ctx.moveTo(i + 2.5, 0);
ctx.lineTo(i + 2.5, canvas.height);
ctx.stroke();
drawGrid();
}
window.addEventListener('resize', draw);
var last = 0;
function step(timestamp) {
var delta = timestamp - last;
if (delta > 40) {
draw();
i = (i + 1) % canvas.width;
last = timestamp;
};
window.requestAnimationFrame(step);
}
window.requestAnimationFrame(step);
return {
'el': canvas,
'addSignal': function(signal) {
signals.push(signal);
signal.resize(canvas.width, canvas.height);
},
'removeSignal': function(signal) {
signals.splice(signals.indexOf(signal), 1);
},
};
}
function Neighbour(iface, addr, color, destroy) {
var th = iface.table.firstElementChild;
var el = iface.table.insertRow();
var tdHostname = el.insertCell();
tdHostname.setAttribute('data-label', th.children[0].textContent);
if (iface.wireless) {
var marker = document.createElement("span");
marker.textContent = "⬤ ";
marker.style.color = color;
tdHostname.appendChild(marker);
}
var hostname = document.createElement("span");
var addr;
hostname.textContent = addr;
tdHostname.appendChild(hostname);
var meshAttrs = {};
function add_attr(attr) {
var key = attr.getAttribute('data-key');
if (!key)
return;
var suffix = attr.getAttribute('data-suffix') || '';
var td = el.insertCell();
td.textContent = '-';
td.setAttribute('data-label', attr.textContent);
meshAttrs[key] = {
'td': td,
'suffix': suffix,
};
}
for (var i = 0; i < th.children.length; i++)
add_attr(th.children[i]);
var tdSignal;
var tdDistance;
var tdInactive;
var signal;
if (iface.wireless) {
tdSignal = el.insertCell();
tdSignal.textContent = '-';
tdSignal.setAttribute(
'data-label',
th.children[Object.keys(meshAttrs).length + 1].textContent
);
tdDistance = el.insertCell();
tdDistance.textContent = '-';
tdDistance.setAttribute(
'data-label',
th.children[Object.keys(meshAttrs).length + 2].textContent
);
tdInactive = el.insertCell();
tdInactive.textContent = '-';
tdInactive.setAttribute(
'data-label',
th.children[Object.keys(meshAttrs).length + 3].textContent
);
signal = Signal(color);
iface.signalgraph.addSignal(signal);
}
el.onmouseenter = function () {
el.classList.add("highlight");
if (signal)
signal.highlight = true;
}
el.onmouseleave = function () {
el.classList.remove("highlight")
if (signal)
signal.highlight = false;
}
var timeout;
function updated() {
if (timeout)
window.clearTimeout(timeout);
timeout = window.setTimeout(function() {
if (signal)
iface.signalgraph.removeSignal(signal);
el.parentNode.removeChild(el);
destroy();
}, 60000);
}
updated();
function address_to_groups(addr) {
if (addr.slice(0, 2) == '::')
addr = '0' + addr;
if (addr.slice(-2) == '::')
addr = addr + '0';
var parts = addr.split(':');
var n = parts.length;
var groups = [];
parts.forEach(function(part, i) {
if (part === '') {
while (n++ <= 8)
groups.push(0);
} else {
if (!/^[a-f0-9]{1,4}$/i.test(part))
return;
groups.push(parseInt(part, 16));
}
});
return groups;
}
function address_to_binary(addr) {
var groups = address_to_groups(addr);
if (!groups)
return;
var ret = '';
groups.forEach(function(group) {
ret += ('0000000000000000' + group.toString(2)).slice(-16);
});
return ret;
}
function common_length(a, b) {
var i;
for (i = 0; i < a.length && i < b.length; i++) {
if (a[i] !== b[i])
break;
}
return i;
}
function choose_address(addresses) {
var node_bin = address_to_binary(node_address);
if (!addresses || !addresses[0])
return;
addresses = addresses.map(function(addr) {
var addr_bin = address_to_binary(addr);
if (!addr_bin)
return [-1];
var common_prefix = 0;
if (node_bin)
common_prefix = common_length(node_bin, addr_bin);
return [common_prefix, addr_bin, addr];
});
addresses.sort(function(a, b) {
if (a[0] < b[0])
return 1;
else if (a[0] > b[0])
return -1;
else if (a[1] < b[1])
return -1;
else if (a[1] > b[1])
return 1;
else
return 0;
});
var address = addresses[0][2];
if (address && !/^fe80:/i.test(address))
return address;
}
return {
'get_hostname': function() {
return hostname.textContent;
},
'get_addr': function() {
return addr;
},
'update_nodeinfo': function(nodeinfo) {
addr = choose_address(nodeinfo.network.addresses);
if (addr) {
if (hostname.nodeName.toLowerCase() === 'span') {
var oldHostname = hostname;
hostname = document.createElement('a');
oldHostname.parentNode.replaceChild(hostname, oldHostname);
}
hostname.href = 'http://[' + addr + ']/';
}
hostname.textContent = nodeinfo.hostname;
if (location && nodeinfo.location) {
var distance = haversine(
location.latitude, location.longitude,
nodeinfo.location.latitude, nodeinfo.location.longitude
);
tdDistance.textContent = Math.round(distance * 1000) + " m"
}
updated();
},
'update_mesh': function(mesh) {
Object.keys(meshAttrs).forEach(function(key) {
var attr = meshAttrs[key];
attr.td.textContent = mesh[key] + attr.suffix;
});
updated();
},
'update_wifi': function(wifi) {
var inactiveLimit = 200;
tdSignal.textContent = wifi.signal;
tdInactive.textContent = Math.round(wifi.inactive / 1000) + ' s';
el.classList.toggle('inactive', wifi.inactive > inactiveLimit);
signal.set(wifi.inactive > inactiveLimit ? null : wifi.signal);
updated();
},
};
}
function Interface(el, ifname, wireless) {
var neighs = {};
var signalgraph;
if (wireless) {
signalgraph = SignalGraph();
el.appendChild(signalgraph.el);
}
var info = {
'table': el.firstElementChild,
'signalgraph': signalgraph,
'ifname': ifname,
'wireless': wireless,
};
var nodeinfo_running = false;
var want_nodeinfo = {};
var graphColors = [];
function get_color() {
if (!graphColors[0])
graphColors = ["#396AB1", "#DA7C30", "#3E9651", "#CC2529", "#535154", "#6B4C9A", "#922428", "#948B3D"];
return graphColors.shift();
}
function neigh_addresses(nodeinfo) {
var addrs = [];
var mesh = nodeinfo.network.mesh;
Object.keys(mesh).forEach(function(meshif) {
var ifaces = mesh[meshif].interfaces;
Object.keys(ifaces).forEach(function(ifaceType) {
ifaces[ifaceType].forEach(function(addr) {
addrs.push(addr);
});
});
});
return addrs;
}
function load_nodeinfo() {
if (nodeinfo_running)
return;
nodeinfo_running = true;
var source = new EventSource('/cgi-bin/dyn/neighbours-nodeinfo?' + encodeURIComponent(ifname));
source.addEventListener("neighbour", function(m) {
try {
var data = JSON.parse(m.data);
neigh_addresses(data).forEach(function(addr) {
var neigh = neighs[addr];
if (neigh) {
delete want_nodeinfo[addr];
try {
neigh.update_nodeinfo(data);
} catch (e) {
console.error(e);
}
}
});
} catch (e) {
console.error(e);
}
}, false);
source.onerror = function() {
source.close();
nodeinfo_running = false;
Object.keys(want_nodeinfo).forEach(function (addr) {
if (want_nodeinfo[addr] > 0) {
want_nodeinfo[addr]--;
load_nodeinfo();
}
});
}
}
function lookup_neigh(addr) {
return neighs[addr];
}
function get_neigh(addr) {
var neigh = neighs[addr];
if (!neigh) {
want_nodeinfo[addr] = 3;
neigh = neighs[addr] = Neighbour(info, addr, get_color(), function() {
delete want_nodeinfo[addr];
delete neighs[addr];
});
load_nodeinfo();
}
return neigh;
}
if (wireless) {
add_event_source('/cgi-bin/dyn/stations?' + encodeURIComponent(ifname), function(data) {
Object.keys(data).forEach(function (addr) {
var wifi = data[addr];
get_neigh(addr).update_wifi(wifi);
});
});
}
return {
'get_neigh': get_neigh,
'lookup_neigh': lookup_neigh
};
}
document.querySelectorAll('[data-interface]').forEach(function(elem) {
var ifname = elem.getAttribute('data-interface');
var address = elem.getAttribute('data-interface-address');
var wireless = !!elem.getAttribute('data-interface-wireless');
interfaces[ifname] = Interface(elem, ifname, wireless);
});
var mesh_provider = document.body.getAttribute('data-mesh-provider');
if (mesh_provider) {
add_event_source(mesh_provider, function(data) {
Object.keys(data).forEach(function (addr) {
var mesh = data[addr];
var iface = interfaces[mesh.ifname];
if (!iface)
return;
iface.get_neigh(addr).update_mesh(mesh);
});
});
}
})();