Skip to content
Snippets Groups Projects
status-page.js 18.1 KiB
Newer Older
  • Learn to ignore specific revisions
  • /*
    	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;
    
    
    		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.sort(function (a, b) {
    			return a.phy - b.phy;
    		});
    
    
    		wireless.forEach(function (radio) {
    			var tr = document.createElement('tr');
    
    			var th = document.createElement('th');
    
    			th.textContent = "phy" + radio.phy;
    
    			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 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;