gluon/package/gluon-status-page/javascript/status-page.js
2021-04-22 21:43:55 +02:00

850 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;
return 'via ' + neigh.get_hostname() + ' (' + i + ')';
}
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);
});
}
function update_radios(wireless) {
function channel(frequency) {
if (frequency===2484)
return 14
if (2412<=frequency && frequency<=2472)
return (frequency-2407)/5
if (5160<=frequency && frequency<=5885)
return (frequency-5000)/5
return 'unknown'
}
var div = document.getElementById('radios');
if (!wireless) {
div.style.display = 'none';
return;
}
div.style.display = '';
var table = document.getElementById('radio-devices');
while (table.lastChild)
table.removeChild(table.lastChild);
wireless.forEach(function (radio) {
var tr = document.createElement('tr');
var th = document.createElement('th');
// TODO enhancement possible, as soon as #2204 is resolved
// (use actual radio names)
th.textContent = "radio";
tr.appendChild(th);
var td = document.createElement('td');
td.innerHTML = radio.frequency + " MHz<br />Channel " + channel(radio.frequency);
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 text = formats[format](value, valuePrev, diff);
if (text !== undefined)
elem.textContent = text;
} catch (e) {
console.error(e);
}
});
try {
update_mesh_vpn(data.mesh_vpn);
} catch (e) {
console.error(e);
}
try {
update_radios(data.wireless);
} 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");
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;
},
'update_nodeinfo': function(nodeinfo) {
var 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);
});
});
}
})();