2018-02-26 00:35:11 +00:00
|
|
|
/*
|
|
|
|
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'));
|
|
|
|
|
2018-04-15 08:00:40 +00:00
|
|
|
String.prototype.sprintf = function() {
|
2018-02-26 00:35:11 +00:00
|
|
|
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;
|
|
|
|
|
2018-04-16 06:48:06 +00:00
|
|
|
if (d === undefined)
|
|
|
|
return "- ";
|
|
|
|
|
2018-02-26 00:35:11 +00:00
|
|
|
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) {
|
2018-08-24 00:22:11 +00:00
|
|
|
var usage = 1 - memory.available / memory.total
|
2018-02-26 00:35:11 +00:00
|
|
|
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);
|
|
|
|
},
|
2019-12-05 16:00:59 +00:00
|
|
|
'neighbour': function(addr) {
|
|
|
|
if (!addr)
|
|
|
|
return '';
|
|
|
|
|
|
|
|
for (var i in interfaces) {
|
|
|
|
var iface = interfaces[i];
|
|
|
|
var neigh = iface.lookup_neigh(addr);
|
|
|
|
if (!neigh)
|
|
|
|
continue;
|
2021-07-23 17:10:27 +00:00
|
|
|
|
|
|
|
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;
|
2019-12-05 16:00:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return 'via ' + addr + ' (unknown iface)';
|
|
|
|
}
|
2018-02-26 00:35:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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');
|
2022-04-03 10:33:19 +00:00
|
|
|
while (table.firstElementChild.lastChild)
|
|
|
|
table.firstElementChild.removeChild(table.firstElementChild.lastChild);
|
2018-02-26 00:35:11 +00:00
|
|
|
|
|
|
|
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);
|
|
|
|
|
2022-04-03 10:33:19 +00:00
|
|
|
table.firstElementChild.appendChild(tr);
|
2018-02-26 00:35:11 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2021-07-23 17:10:27 +00:00
|
|
|
var format_result = formats[format](value, valuePrev, diff);
|
|
|
|
switch (typeof format_result) {
|
|
|
|
case "object":
|
|
|
|
if (elem.lastChild)
|
|
|
|
elem.removeChild(elem.lastChild);
|
|
|
|
elem.appendChild(format_result);
|
|
|
|
break;
|
2021-08-08 10:08:50 +00:00
|
|
|
default:
|
|
|
|
elem.textContent = format_result;
|
2021-07-23 17:10:27 +00:00
|
|
|
}
|
2018-02-26 00:35:11 +00:00
|
|
|
} 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 {
|
2021-10-26 10:05:59 +00:00
|
|
|
lastImage = ctx.getImageData(0, 0, w, h);
|
2018-02-26 00:35:11 +00:00
|
|
|
} 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) {
|
2022-04-03 10:33:19 +00:00
|
|
|
var th = iface.table.firstElementChild.firstElementChild;
|
|
|
|
var el = iface.table.firstElementChild.insertRow();
|
2018-02-26 00:35:11 +00:00
|
|
|
|
|
|
|
var tdHostname = el.insertCell();
|
2020-04-07 09:45:00 +00:00
|
|
|
tdHostname.setAttribute('data-label', th.children[0].textContent);
|
2018-02-26 00:35:11 +00:00
|
|
|
|
|
|
|
if (iface.wireless) {
|
|
|
|
var marker = document.createElement("span");
|
|
|
|
marker.textContent = "⬤ ";
|
|
|
|
marker.style.color = color;
|
|
|
|
tdHostname.appendChild(marker);
|
|
|
|
}
|
|
|
|
|
|
|
|
var hostname = document.createElement("span");
|
2021-07-23 17:10:27 +00:00
|
|
|
var addr;
|
2018-02-26 00:35:11 +00:00
|
|
|
hostname.textContent = addr;
|
|
|
|
tdHostname.appendChild(hostname);
|
|
|
|
|
2018-04-15 09:36:37 +00:00
|
|
|
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 = '-';
|
2020-04-07 09:45:00 +00:00
|
|
|
td.setAttribute('data-label', attr.textContent);
|
2018-04-15 09:36:37 +00:00
|
|
|
|
|
|
|
meshAttrs[key] = {
|
|
|
|
'td': td,
|
|
|
|
'suffix': suffix,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
for (var i = 0; i < th.children.length; i++)
|
|
|
|
add_attr(th.children[i]);
|
2018-02-26 00:35:11 +00:00
|
|
|
|
|
|
|
var tdSignal;
|
|
|
|
var tdDistance;
|
|
|
|
var tdInactive;
|
|
|
|
var signal;
|
|
|
|
|
|
|
|
if (iface.wireless) {
|
|
|
|
tdSignal = el.insertCell();
|
|
|
|
tdSignal.textContent = '-';
|
2020-04-07 09:45:00 +00:00
|
|
|
tdSignal.setAttribute(
|
|
|
|
'data-label',
|
|
|
|
th.children[Object.keys(meshAttrs).length + 1].textContent
|
|
|
|
);
|
|
|
|
|
2018-02-26 00:35:11 +00:00
|
|
|
tdDistance = el.insertCell();
|
|
|
|
tdDistance.textContent = '-';
|
2020-04-07 09:45:00 +00:00
|
|
|
tdDistance.setAttribute(
|
|
|
|
'data-label',
|
|
|
|
th.children[Object.keys(meshAttrs).length + 2].textContent
|
|
|
|
);
|
|
|
|
|
2018-02-26 00:35:11 +00:00
|
|
|
tdInactive = el.insertCell();
|
|
|
|
tdInactive.textContent = '-';
|
2020-04-07 09:45:00 +00:00
|
|
|
tdInactive.setAttribute(
|
|
|
|
'data-label',
|
|
|
|
th.children[Object.keys(meshAttrs).length + 3].textContent
|
|
|
|
);
|
2018-02-26 00:35:11 +00:00
|
|
|
|
|
|
|
signal = Signal(color);
|
|
|
|
iface.signalgraph.addSignal(signal);
|
|
|
|
}
|
|
|
|
|
|
|
|
el.onmouseenter = function () {
|
|
|
|
el.classList.add("highlight");
|
|
|
|
if (signal)
|
|
|
|
signal.highlight = true;
|
2021-10-26 10:05:59 +00:00
|
|
|
};
|
2018-02-26 00:35:11 +00:00
|
|
|
|
|
|
|
el.onmouseleave = function () {
|
2021-10-26 10:05:59 +00:00
|
|
|
el.classList.remove("highlight");
|
2018-02-26 00:35:11 +00:00
|
|
|
if (signal)
|
|
|
|
signal.highlight = false;
|
2021-10-26 10:05:59 +00:00
|
|
|
};
|
2018-02-26 00:35:11 +00:00
|
|
|
|
|
|
|
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 = [];
|
|
|
|
|
2021-10-25 13:25:15 +00:00
|
|
|
for (var i = 0; i < parts.length; i++) {
|
|
|
|
var part = parts[i];
|
2018-02-26 00:35:11 +00:00
|
|
|
if (part === '') {
|
|
|
|
while (n++ <= 8)
|
|
|
|
groups.push(0);
|
|
|
|
} else {
|
|
|
|
if (!/^[a-f0-9]{1,4}$/i.test(part))
|
|
|
|
return;
|
|
|
|
|
|
|
|
groups.push(parseInt(part, 16));
|
|
|
|
}
|
2021-10-25 13:25:15 +00:00
|
|
|
};
|
2018-02-26 00:35:11 +00:00
|
|
|
|
|
|
|
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 {
|
2019-12-05 16:00:59 +00:00
|
|
|
'get_hostname': function() {
|
|
|
|
return hostname.textContent;
|
|
|
|
},
|
2021-07-23 17:10:27 +00:00
|
|
|
'get_addr': function() {
|
|
|
|
return addr;
|
|
|
|
},
|
2018-02-26 00:35:11 +00:00
|
|
|
'update_nodeinfo': function(nodeinfo) {
|
2021-07-23 17:10:27 +00:00
|
|
|
addr = choose_address(nodeinfo.network.addresses);
|
2018-02-26 00:35:11 +00:00
|
|
|
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) {
|
2018-04-15 09:36:37 +00:00
|
|
|
Object.keys(meshAttrs).forEach(function(key) {
|
|
|
|
var attr = meshAttrs[key];
|
|
|
|
attr.td.textContent = mesh[key] + attr.suffix;
|
|
|
|
});
|
2018-02-26 00:35:11 +00:00
|
|
|
|
|
|
|
updated();
|
|
|
|
},
|
|
|
|
'update_wifi': function(wifi) {
|
|
|
|
var inactiveLimit = 200;
|
|
|
|
|
|
|
|
tdSignal.textContent = wifi.signal;
|
2021-04-14 11:36:54 +00:00
|
|
|
tdInactive.textContent = Math.round(wifi.inactive / 1000) + ' s';
|
2018-02-26 00:35:11 +00:00
|
|
|
|
2021-04-14 11:36:54 +00:00
|
|
|
el.classList.toggle('inactive', wifi.inactive > inactiveLimit);
|
2018-02-26 00:35:11 +00:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-05 16:00:59 +00:00
|
|
|
function lookup_neigh(addr) {
|
|
|
|
return neighs[addr];
|
|
|
|
}
|
2018-02-26 00:35:11 +00:00
|
|
|
|
|
|
|
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,
|
2019-12-05 16:00:59 +00:00
|
|
|
'lookup_neigh': lookup_neigh
|
2018-02-26 00:35:11 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
});
|
|
|
|
|
2018-04-15 09:36:37 +00:00
|
|
|
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;
|
2018-02-26 00:35:11 +00:00
|
|
|
|
2018-04-15 09:36:37 +00:00
|
|
|
iface.get_neigh(addr).update_mesh(mesh);
|
|
|
|
});
|
2018-02-26 00:35:11 +00:00
|
|
|
});
|
2018-04-15 09:36:37 +00:00
|
|
|
}
|
2018-02-26 00:35:11 +00:00
|
|
|
})();
|