Let gluon-respondd expose "MemAvailable" from /proc/meminfo to allow for a more realistic memory-usage estimation. Information on MemAvailable can be found here: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/ commit/?id=34e431b0ae398fc54ea69ff85ec700722c9da773
		
			
				
	
	
		
			766 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			766 lines
		
	
	
		
			16 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);
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 
 | |
| 	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 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);
 | |
| 		}
 | |
| 	})
 | |
| 
 | |
| 	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();
 | |
| 
 | |
| 		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 = '-';
 | |
| 
 | |
| 			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 = '-';
 | |
| 			tdDistance = el.insertCell();
 | |
| 			tdDistance.textContent = '-';
 | |
| 			tdInactive = el.insertCell();
 | |
| 			tdInactive.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 {
 | |
| 			'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 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,
 | |
| 		};
 | |
| 	}
 | |
| 
 | |
| 	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);
 | |
| 			});
 | |
| 		});
 | |
| 	}
 | |
| })();
 |