gluon-status-page: reimplement based on gluon-web

This new status page is significantly smaller than the old one. It always
loads its resources from the same host as the page itself, not requiring
cross-origin requests anymore.

It also uses the common i18n infrastructure of gluon-web.

Fixes #914
This commit is contained in:
Matthias Schiffer 2018-02-26 01:35:11 +01:00
parent b1aa5390a7
commit 88906f238b
No known key found for this signature in database
GPG Key ID: 16EF3F64CB201D9C
32 changed files with 1402 additions and 2044 deletions

View File

@ -1,78 +1,38 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=gluon-status-page PKG_NAME:=gluon-status-page
PKG_VERSION:=2 PKG_VERSION:=3
PKG_RELEASE:=1
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME) include ../gluon.mk
PKG_BUILD_DEPENDS:=node/host
include $(INCLUDE_DIR)/package.mk PKG_CONFIG_DEPENDS += $(GLUON_I18N_CONFIG)
RJS_VERSION:=2.1.10
RJS:=r-$(RJS_VERSION).js
define Download/rjs
FILE:=$(RJS)
URL:=http://requirejs.org/docs/release/$(RJS_VERSION)
URL_FILE:=r.js
HASH:=d0b7cfd962a7f8ac52a5d528df486341eed856609d9c75fa2566a32900f5b143
endef
$(eval $(call Download,rjs))
BACON_VERSION:=0.7.71
BACON:=Bacon-$(BACON_VERSION).js
define Download/Bacon
FILE:=$(BACON)
URL:=http://cdnjs.cloudflare.com/ajax/libs/bacon.js/$(BACON_VERSION)
URL_FILE:=Bacon.js
HASH:=93d840d2167964ced7c53598f7d07151c3bfb1d8a7c3e8cff44cadd7dea25f1d
endef
$(eval $(call Download,Bacon))
ALMOND_VERSION:=0.3.1
ALMOND:=almond-$(ALMOND_VERSION).js
define Download/almond
FILE:=$(ALMOND)
URL:=https://raw.githubusercontent.com/jrburke/almond/$(ALMOND_VERSION)
URL_FILE:=almond.js
HASH:=3df2baac13da29dab646f9b9ddd2c5e09d91a49ae3a4f3befb40ce1dd60937f2
endef
$(eval $(call Download,almond))
define Package/gluon-status-page define Package/gluon-status-page
SECTION:=gluon SECTION:=gluon
CATEGORY:=Gluon CATEGORY:=Gluon
TITLE:=Adds a status page showing information about the node. TITLE:=Status page showing information about the node
DEPENDS:=+gluon-status-page-api DEPENDS:=+gluon-web +gluon-status-page-api
endef
define Package/gluon-status-page/description
Adds a status page showing information about the node.
Especially useful in combination with the next-node feature.
endef endef
define Build/Prepare define Build/Prepare
mkdir -p $(PKG_BUILD_DIR) mkdir -p $(PKG_BUILD_DIR)
$(CP) $(DL_DIR)/$(RJS) $(PKG_BUILD_DIR)/r.js
$(CP) $(DL_DIR)/$(BACON) $(PKG_BUILD_DIR)/Bacon.js
$(CP) $(DL_DIR)/$(ALMOND) $(PKG_BUILD_DIR)/almond.js
endef
define Build/Configure
$(CP) ./src/* $(PKG_BUILD_DIR)/
endef endef
define Build/Compile define Build/Compile
cd $(PKG_BUILD_DIR) && \ $(call GluonSrcDiet,./luasrc,$(PKG_BUILD_DIR)/luadest/)
node r.js -o build.js && \ $(call GluonBuildI18N,gluon-status-page,i18n)
node r.js -o cssIn=css/main.css out=style.css && \
$(M4) index.html.m4 > index.html
endef endef
define Package/gluon-status-page/install define Package/gluon-status-page/install
$(INSTALL_DIR) $(1)/lib/gluon/status-page/www/ $(CP) ./files/* $(1)/
$(INSTALL_DATA) $(PKG_BUILD_DIR)/index.html $(1)/lib/gluon/status-page/www/ $(CP) $(PKG_BUILD_DIR)/luadest/* $(1)/
$(INSTALL_DIR) $(1)/lib/gluon/status-page/view/
$(LN) /lib/gluon/web/i18n $(1)/lib/gluon/status-page/
$(LN) /lib/gluon/web/view/error $(1)/lib/gluon/status-page/view/
$(call GluonInstallI18N,gluon-status-page,$(1))
endef endef
$(eval $(call BuildPackage,gluon-status-page)) $(eval $(call BuildPackage,gluon-status-page))

View File

@ -0,0 +1,24 @@
<%-
http:prepare_content("application/xhtml+xml")
-%>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no" />
<title><%:Error%></title>
<link rel="stylesheet" href="/static/status-page.css" type="text/css" />
</head>
<body>
<header>
<h1><%:Error%></h1>
</header>
<div class="container">
<div class="frame">
<% renderer.render(content, scope, pkg) %>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,162 @@
<%-
local fs = require 'nixio.fs'
local json = require 'jsonc'
local ubus = require 'ubus'
local util = require 'gluon.util'
local translations = {}
local function _(v)
translations[v] = translate(v)
end
-- i18n strings for JavaScript
_('.') -- decimal point
_('connected')
_('not connected')
_('1 day')
_('%s days')
_('%s used')
_('%s packets/s')
local function get_interfaces()
local uconn = ubus.connect()
if not uconn then
error('failed to connect to ubus')
end
local interfaces = util.get_mesh_devices(uconn)
ubus.close(uconn)
table.sort(interfaces)
return interfaces
end
local function is_wireless(iface)
while true do
local pattern = '/sys/class/net/' .. iface .. '/lower_*'
local lower = fs.glob(pattern)()
if not lower then break end
iface = lower:sub(pattern:len())
end
return fs.access('/sys/class/net/' .. iface .. '/wireless') ~= nil
end
local interfaces = get_interfaces()
local nodeinfo = json.parse(util.exec('exec gluon-neighbour-info -d ::1 -p 1001 -t 1 -c 1 -r nodeinfo'))
local function sorted(t)
t = {unpack(t)}
table.sort(t)
return t
end
local function enabled(v)
return v and translate('enabled') or translate('disabled')
end
local function statistics(key, format)
return string.format('<span data-statistics="%s" data-format="%s"></span>', pcdata(key), pcdata(format or 'id'))
end
local function statisticsTraffic(key)
return string.format('%s<br />%s<br />%s',
statistics(key .. '/packets', 'packetsDiff'),
statistics(key .. '/bytes', 'bytesDiff'),
statistics(key .. '/bytes', 'bytes')
)
end
http:prepare_content("application/xhtml+xml")
-%>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no" />
<title><%| nodeinfo.hostname %> - <%:Status%></title>
<link rel="stylesheet" href="/static/status-page.css" type="text/css" />
</head>
<body data-node-address="<%| http:getenv('SERVER_ADDR') %>"<%= attr('data-translations', translations) .. attr('data-node-location', nodeinfo.location) %>>
<header>
<h1><%| nodeinfo.hostname %></h1>
</header>
<div class="container">
<div class="frame">
<h2><%:Overview%></h2>
<dl>
<dt><%:Node name%></dt><dd><%| nodeinfo.hostname %></dd>
<dt><%:Model%></dt><dd><%| nodeinfo.hardware.model %></dd>
<dt><%:Primary MAC address%></dt><dd><%| nodeinfo.network.mac %></dd>
<dt><%:IP address%></dt><dd><%= pcdata(table.concat(sorted(nodeinfo.network.addresses), '\n')):gsub('\n', '<br />') %></dd>
<dt><%:Firmware%></dt><dd><%| nodeinfo.software.firmware.release %></dd>
<% if nodeinfo.software.fastd then -%>
<dt><%:Mesh VPN%></dt><dd><%| enabled(nodeinfo.software.fastd.enabled) %></dd>
<%- end %>
<% if nodeinfo.software.autoupdater then -%>
<dt><%:Automatic updates%></dt><dd><%| enabled(nodeinfo.software.autoupdater.enabled) %><%|
nodeinfo.software.autoupdater.enabled and
string.format(' (%s)', nodeinfo.software.autoupdater.branch)
%></dd>
<%- end %>
</dl>
</div>
<div class="frame">
<h2><%:Monitoring%></h2>
<table>
<tr><th><%:Uptime%></th><td><%= statistics('uptime', 'time') %></td></tr>
<tr><th><%:Load average%></th><td><%= statistics('loadavg', 'decimal') %></td></tr>
<tr><th><%:RAM%></th><td><%= statistics('memory', 'memory') %></td></tr>
<tr><th><%:Filesystem%></th><td><%= statistics('rootfs_usage', 'percent') %></td></tr>
<tr><th><%:Gateway%></th><td><%= statistics('gateway') %></td></tr>
<tr><th><%:Clients%></th><td><%= statistics('clients/total') %></td></tr>
</table>
<h3><%:Traffic%></h3>
<table>
<tr><th><%:Transmitted%></th><td><%= statisticsTraffic('traffic/tx') %></td></tr>
<tr><th><%:Received%></th><td><%= statisticsTraffic('traffic/rx') %></td></tr>
<tr><th><%:Forwarded%></th><td><%= statisticsTraffic('traffic/forward') %></td></tr>
</table>
<div id="mesh-vpn" style="display: none">
<h3><%:Mesh VPN%></h3>
<table id="mesh-vpn-peers">
</table>
</div>
</div>
<div class="frame">
<h2><%:Neighbors%></h2>
<%
for _, iface in ipairs(interfaces) do
local wireless = is_wireless(iface)
local address = fs.readfile('/sys/class/net/' .. iface .. '/address')
if address then
%>
<h3><%| iface %></h3>
<div data-interface="<%| iface %>" data-interface-address="<%| util.trim(address) %>"<%= attr('data-interface-wireless', wireless) %>>
<table class="datatable">
<tr>
<th><%:Node%></th>
<th>TQ</th>
<% if wireless then %>
<th>dBm</th>
<th><%:Distance%></th>
<th><%:Last seen%></th>
<% end %>
</tr>
</table>
</div>
<%
end
end
%>
</div>
</div>
<script src="/static/status-page.js"></script>
</body>
</html>

View File

@ -0,0 +1,8 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="refresh" content="0; URL=/cgi-bin/status" />
</head>
<body>
</body>
</html>

View File

@ -0,0 +1 @@
html,body,div,span,h1,h2,h3,dl,dt,dd,canvas,header,table,tr,th,td{margin:0;padding:0;border:0;outline:0;font-size:100%;vertical-align:baseline;background:transparent}body{background:rgba(0,0,0,0.12);font-family:Roboto, Lucida Grande, sans, Arial;color:rgba(0,0,0,0.87);font-size:14px;line-height:1}a{color:rgba(220,0,103,0.87);text-decoration:none;margin:0;padding:0;font-size:100%;vertical-align:baseline;background:transparent}a:hover{text-decoration:underline}h1{font-weight:bold}h2{font-size:16px;margin-bottom:16px;color:rgba(0,0,0,0.54)}h3{font-size:15px;margin-top:16px;margin-bottom:8px;color:rgba(0,0,0,0.54)}header{display:flex;padding:0 14px;background:#dc0067;color:rgba(255,255,255,0.98);position:absolute;top:0;width:100%;box-sizing:border-box;height:20vh;z-index:-1;box-shadow:0px 5px 6px rgba(0,0,0,0.16),0px 1.5px 3px rgba(0,0,0,0.23);white-space:nowrap}header h1{font-size:24px;margin:10px 0;padding:6px 0;text-overflow:ellipsis;overflow:hidden;flex:1}.container{display:flex;max-width:90vw;margin:64px auto 24px auto;background:#fdfdfd;box-shadow:0px 5px 20px rgba(0,0,0,0.19),0px 3px 6px rgba(0,0,0,0.23)}.container>.frame{flex:1;border-style:solid;border-color:rgba(0,0,0,0.12);box-sizing:border-box;padding:16px}.container>.frame+.frame{border-width:0 0 0 1px}dt,th{font-weight:bold;color:rgba(0,0,0,0.87)}dt{margin-bottom:4px}th,td{text-align:left;padding:4px 16px 4px 0}th:last-child,td:last-child{padding-right:0}dd,td{font-weight:normal;font-size:0.9em;color:rgba(0,0,0,0.54)}dd{margin-bottom:16px}table{border-collapse:collapse;border-spacing:0}table.datatable{width:100%}table.datatable th,table.datatable td{font-size:1em;white-space:nowrap}table.datatable th:last-child,table.datatable td:last-child{width:100%}table.datatable tr.inactive{opacity:0.33}table.datatable tr.highlight{background:rgba(255,180,0,0.25)}canvas.signalgraph{margin-top:8px;width:100%}@media only screen and (max-width: 1250px){.container{max-width:none;margin:56px 0 0}header{height:56px;z-index:1;position:fixed}}@media only screen and (max-width: 700px){.container{display:block}.container>.frame+.frame{border-width:1px 0 0 0}}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,113 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"PO-Revision-Date: 2018-02-26 00:30+0100\n"
"Last-Translator: <mschiffer@universe-factory.net>\n"
"Language-Team: German\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "%s days"
msgstr "%s Tage"
msgid "%s packets/s"
msgstr "%s Pakete/s"
msgid "%s used"
msgstr "%s belegt"
msgid "."
msgstr ","
msgid "1 day"
msgstr "1 Tag"
msgid "Automatic updates"
msgstr "Automatische Updates"
msgid "Clients"
msgstr "Clients"
msgid "Distance"
msgstr "Entfernung"
msgid "Error"
msgstr "Fehler"
msgid "Filesystem"
msgstr "Dateisystem"
msgid "Firmware"
msgstr "Firmware"
msgid "Forwarded"
msgstr "Weitergeleitet"
msgid "Gateway"
msgstr "Gateway"
msgid "IP address"
msgstr "IP-Adresse"
msgid "Last seen"
msgstr "Zuletzt gesehen"
msgid "Load average"
msgstr "Systemlast"
msgid "Mesh VPN"
msgstr "Mesh-VPN"
msgid "Model"
msgstr "Modell"
msgid "Monitoring"
msgstr ""
msgid "Neighbors"
msgstr "Nachbarknoten"
msgid "Node"
msgstr "Knoten"
msgid "Node name"
msgstr "Knotenname"
msgid "Overview"
msgstr "Übersicht"
msgid "Primary MAC address"
msgstr "Primäre MAC-Adresse"
msgid "RAM"
msgstr "RAM"
msgid "Received"
msgstr "Empfangen"
msgid "Status"
msgstr "Status"
msgid "Traffic"
msgstr ""
msgid "Transmitted"
msgstr "Gesendet"
msgid "Uptime"
msgstr "Laufzeit"
msgid "connected"
msgstr "verbunden"
msgid "disabled"
msgstr "deaktiviert"
msgid "enabled"
msgstr "aktiviert"
msgid "not connected"
msgstr "nicht verbunden"

View File

@ -0,0 +1,104 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
msgid "%s days"
msgstr ""
msgid "%s packets/s"
msgstr ""
msgid "%s used"
msgstr ""
msgid "."
msgstr ""
msgid "1 day"
msgstr ""
msgid "Automatic updates"
msgstr ""
msgid "Clients"
msgstr ""
msgid "Distance"
msgstr ""
msgid "Error"
msgstr ""
msgid "Filesystem"
msgstr ""
msgid "Firmware"
msgstr ""
msgid "Forwarded"
msgstr ""
msgid "Gateway"
msgstr ""
msgid "IP address"
msgstr ""
msgid "Last seen"
msgstr ""
msgid "Load average"
msgstr ""
msgid "Mesh VPN"
msgstr ""
msgid "Model"
msgstr ""
msgid "Monitoring"
msgstr ""
msgid "Neighbors"
msgstr ""
msgid "Node"
msgstr ""
msgid "Node name"
msgstr ""
msgid "Overview"
msgstr ""
msgid "Primary MAC address"
msgstr ""
msgid "RAM"
msgstr ""
msgid "Received"
msgstr ""
msgid "Status"
msgstr ""
msgid "Traffic"
msgstr ""
msgid "Transmitted"
msgstr ""
msgid "Uptime"
msgstr ""
msgid "connected"
msgstr ""
msgid "disabled"
msgstr ""
msgid "enabled"
msgstr ""
msgid "not connected"
msgstr ""

View File

@ -0,0 +1,31 @@
THe previous version of the status page had a Russian translation;
if we ever add Russion to gluon-web, the following strings can be reused:
"Node": "Узел",
"Distance": "Дальность",
"Inactive": "Не активен",
"Node name": "Имя узла",
"Contact": "Контакт",
"Model": "Модель",
"Primary MAC": "Основной MAC",
"IP Address": "IP Адрес",
"Automatic updates": "Автоматические обновления",
"Overview": "Обзор",
"used": "используется",
"Uptime": "Время работы",
"Load average": "Загрузка системы",
"Gateway": "Шлюз",
"Clients": "Клиенты",
"Transmitted": "Передано",
"Received": "Получено",
"Forwarded": "Переправленно",
"Day": "День",
"Days": "Дней",
"connected": "подключено",
"not connected": "не подключено",
"Packets/s": "Пакетов/c",
"Statistic": "Статистика",
"Traffic": "Трафик",
"Neighbors": "Соседи",
"Firmware": "Прошивка",
"Branch": "Ветка"

View File

@ -1,100 +0,0 @@
{
"name": "statuspage",
"css_prefix_text": "icon-",
"css_use_suffix": false,
"hinting": true,
"units_per_em": 1000,
"ascent": 850,
"glyphs": [
{
"uid": "12f4ece88e46abd864e40b35e05b11cd",
"css": "ok",
"code": 59397,
"src": "fontawesome"
},
{
"uid": "5211af474d3a9848f67f945e2ccaf143",
"css": "cancel",
"code": 59399,
"src": "fontawesome"
},
{
"uid": "e15f0d620a7897e2035c18c80142f6d9",
"css": "link-ext",
"code": 59407,
"src": "fontawesome"
},
{
"uid": "c76b7947c957c9b78b11741173c8349b",
"css": "attention",
"code": 59403,
"src": "fontawesome"
},
{
"uid": "559647a6f430b3aeadbecd67194451dd",
"css": "menu",
"code": 59392,
"src": "fontawesome"
},
{
"uid": "2d6150442079cbda7df64522dc24f482",
"css": "down-dir",
"code": 59393,
"src": "fontawesome"
},
{
"uid": "80cd1022bd9ea151d554bec1fa05f2de",
"css": "up-dir",
"code": 59394,
"src": "fontawesome"
},
{
"uid": "9dc654095085167524602c9acc0c5570",
"css": "left-dir",
"code": 59395,
"src": "fontawesome"
},
{
"uid": "fb1c799ffe5bf8fb7f8bcb647c8fe9e6",
"css": "right-dir",
"code": 59396,
"src": "fontawesome"
},
{
"uid": "a73c5deb486c8d66249811642e5d719a",
"css": "arrows-cw",
"code": 59400,
"src": "fontawesome"
},
{
"uid": "750058837a91edae64b03d60fc7e81a7",
"css": "ellipsis-vert",
"code": 59401,
"src": "fontawesome"
},
{
"uid": "56a21935a5d4d79b2e91ec00f760b369",
"css": "sort",
"code": 59404,
"src": "fontawesome"
},
{
"uid": "94103e1b3f1e8cf514178ec5912b4469",
"css": "sort-down",
"code": 59405,
"src": "fontawesome"
},
{
"uid": "65b3ce930627cabfb6ac81ac60ec5ae4",
"css": "sort-up",
"code": 59406,
"src": "fontawesome"
},
{
"uid": "cda0cdcfd38f5f1d9255e722dad42012",
"css": "spinner",
"code": 59402,
"src": "fontawesome"
}
]
}

View File

@ -0,0 +1,737 @@
/*
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.free + memory.buffers + memory.cached) / 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 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 tdTQ = el.insertCell();
tdTQ.textContent = '-';
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) {
tdTQ.textContent = Math.round(mesh.tq / 2.55) + ' %';
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);
});
add_event_source('/cgi-bin/dyn/neighbours-batadv', 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);
});
});
})();

View File

@ -0,0 +1,3 @@
entry({}, call(function(http, renderer)
renderer.render('status-page', nil, 'gluon-status-page')
end))

View File

@ -0,0 +1,8 @@
#!/usr/bin/lua
require 'gluon.web.cgi' {
base_path = '/lib/gluon/status-page',
layout_package = 'gluon-status-page',
layout_template = 'layout', -- only used for error pages
}

View File

@ -0,0 +1,195 @@
/*
ATTENTION: This file is not compiled when building gluon.
The compiled version is at ../files/lib/gluon/status-page/www/static/status-page.css
Use sass like this to update it:
sass --sourcemap=none -C -t compressed sass/status-page.scss files/lib/gluon/status-page/www/static/status-page.css
When commiting changes to this file make sure to commit the respective
changes to the compiled version within the same commit!
*/
html, body, div, span,
h1, h2, h3,
dl, dt, dd,
canvas, header,
table, tr, th, td {
margin: 0;
padding: 0;
border: 0;
outline: 0;
font-size: 100%;
vertical-align: baseline;
background: transparent;
}
body {
background: rgba(0, 0, 0, 0.12);
font-family: Roboto, Lucida Grande, sans, Arial;
color: rgba(0, 0, 0, 0.87);
font-size: 14px;
line-height: 1;
}
a {
color: rgba(220, 0, 103, 0.87);
text-decoration: none;
margin: 0;
padding: 0;
font-size: 100%;
vertical-align: baseline;
background: transparent;
&:hover {
text-decoration: underline;
}
}
h1 {
font-weight: bold;
}
h2 {
font-size: 16px;
margin-bottom: 16px;
color: rgba(0, 0, 0, 0.54);
}
h3 {
font-size: 15px;
margin-top: 16px;
margin-bottom: 8px;
color: rgba(0, 0, 0, 0.54);
}
header {
display: flex;
padding: 0 14px;
background: #dc0067;
color: rgba(255, 255, 255, 0.98);
position: absolute;
top: 0;
width: 100%;
box-sizing: border-box;
height: 20vh;
z-index: -1;
box-shadow: 0px 5px 6px rgba(0, 0, 0, 0.16), 0px 1.5px 3px rgba(0, 0, 0, 0.23);
white-space: nowrap;
h1 {
font-size: 24px;
margin: 10px 0;
padding: 6px 0;
text-overflow: ellipsis;
overflow: hidden;
flex: 1;
}
}
.container {
display: flex;
max-width: 90vw;
margin: 64px auto 24px auto;
background: rgb(253, 253, 253);
box-shadow: 0px 5px 20px rgba(0, 0, 0, 0.19), 0px 3px 6px rgba(0, 0, 0, 0.23);
& > .frame {
flex: 1;
border-style: solid;
border-color: rgba(0, 0, 0, 0.12);
box-sizing: border-box;
padding: 16px;
& + .frame {
border-width: 0 0 0 1px;
}
}
}
dt, th {
font-weight: bold;
color: rgba(0, 0, 0, 0.87);
}
dt {
margin-bottom: 4px;
}
th, td {
text-align: left;
padding: 4px 16px 4px 0;
&:last-child {
padding-right: 0;
}
}
dd, td {
font-weight: normal;
font-size: 0.9em;
color: rgba(0, 0, 0, 0.54);
}
dd {
margin-bottom: 16px;
}
table {
border-collapse: collapse;
border-spacing: 0;
&.datatable {
width: 100%;
th, td {
font-size: 1em;
white-space: nowrap;
&:last-child {
width: 100%;
}
}
tr.inactive {
opacity: 0.33;
}
tr.highlight {
background: rgba(255, 180, 0, 0.25);
}
}
}
canvas.signalgraph {
margin-top: 8px;
width: 100%;
}
@media only screen and (max-width: 1250px) {
.container {
max-width: none;
margin: 56px 0 0;
}
header {
height: 56px;
z-index: 1;
position: fixed;
}
}
@media only screen and (max-width: 700px) {
.container {
display: block;
& > .frame + .frame {
border-width: 1px 0 0 0;
}
}
}

View File

@ -1,10 +0,0 @@
({
paths: {
"bacon": "../Bacon"
},
baseUrl: "js/",
name: "../almond",
include: "main",
optimize: "uglify2",
out: "app.js",
})

View File

@ -1,12 +0,0 @@
/*
Animation example, for spinners
*/
.animate-spin {
animation: spin 2s linear infinite;
display: inline-block;
}
@keyframes spin {
100% {
transform: rotate(360deg);
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,171 +0,0 @@
@import "reset.css";
@import "font.css";
@import "menu.css";
@import "animation.css";
body {
background: rgba(0, 0, 0, 0.12);
font-family: Roboto, Lucida Grande, sans, Arial;
color: rgba(0, 0, 0, 0.87);
font-size: 14px;
}
a {
color: rgba(220, 0, 103, 0.87);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
header {
display: flex;
padding: 0 14px;
background: #dc0067;
color: rgba(255, 255, 255, 0.98);
position: absolute;
top: 0;
width: 100%;
box-sizing: border-box;
height: 20vh;
z-index: -1;
box-shadow: 0px 5px 6px rgba(0, 0, 0, 0.16), 0px 1.5px 3px rgba(0, 0, 0, 0.23);
white-space: nowrap;
}
header h1, header .icons {
font-size: 24px;
margin: 10px 0;
padding: 6px 0;
}
header h1 {
text-overflow: ellipsis;
overflow: hidden;
flex: 1;
}
header h1:hover {
text-decoration: underline;
cursor: pointer;
}
h1 {
font-weight: bold;
}
h2, h3 {
font-size: 16px;
color: rgba(0, 0, 0, 0.54);
}
h2 {
padding: 16px 16px;
}
h3 {
padding: 16px 16px 8px;
}
.container {
max-width: 90vw;
margin: 64px auto 24px auto;
background: rgb(253, 253, 253);
box-shadow: 0px 5px 20px rgba(0, 0, 0, 0.19), 0px 3px 6px rgba(0, 0, 0, 0.23);
}
.container .frame {
box-sizing: border-box;
}
.vertical-split {
display: flex;
}
.vertical-split > .frame {
flex: 1;
border-style: solid;
border-color: rgba(0, 0, 0, 0.12);
}
.vertical-split > .frame + .frame {
border-width: 0 0 0 1px;
}
dl, pre {
padding: 0 16px 16px;
}
table {
margin: 0 16px;
}
dt, th {
font-weight: bold;
color: rgba(0, 0, 0, 0.87);
}
dt {
margin-bottom: 4px;
}
th {
text-align: left;
padding: 4px 16px 4px 0;
}
dd, td {
font-weight: normal;
font-size: 0.9em;
color: rgba(0, 0, 0, 0.54);
}
dd {
padding-bottom: 16px;
}
table.datatable {
width: calc(100% - 32px);
}
table.datatable td {
font-size: 1em;
padding: 4px 0;
}
table.datatable tr.inactive {
opacity: 0.33;
}
table.datatable tr.highlight {
background: rgba(255, 180, 0, 0.25);
}
div.signalgraph {
margin: 16px;
}
@media only screen and (max-width: 1250px) {
.container {
max-width: none;
margin: 56px 0 0;
}
header {
height: 56px;
z-index: 1;
position: fixed;
}
}
@media only screen and (max-width: 700px) {
.vertical-split {
display: block;
}
.vertical-split > .frame + .frame {
border-width: 1px 0 0 0;
}
}

View File

@ -1,50 +0,0 @@
.noscroll {
overflow: hidden;
}
.menu-background {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 10;
}
.menu {
background: rgba(255, 255, 255, 1);
position: fixed;
z-index: 11;
padding: 8px 0;
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.12), 0 1px 4px rgba(0, 0, 0, 0.24);
overflow-y: auto;
max-height: 80vh;
transform-origin: top left;
animation: new-menu-animation .08s ease-out forwards;
}
@keyframes new-menu-animation {
from {
transform: scaleY(0);
}
to {
transform: scaleY(1);
}
}
.menu li {
cursor: pointer;
display: block;
font-size: 16px;
padding: 16px 32px 16px 16px;
color: rgba(0, 0, 0, 0.87);
}
.menu li:hover {
background: rgba(0, 0, 0, 0.07);
}
.menu li:active {
background: rgba(0, 0, 0, 0.07);
}

View File

@ -1,86 +0,0 @@
/*
html5doctor.com Reset Stylesheet v1.6.1
Last Updated: 2010-09-17
Author: Richard Clark - http://richclarkdesign.com
*/
html, body, div, span, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
abbr, address, cite, code,
del, dfn, em, img, ins, kbd, q, samp,
small, strong, sub, sup, var,
b, i,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, figcaption, figure,
footer, header, hgroup, menu, nav, section, summary,
time, mark, audio, video {
margin:0;
padding:0;
border:0;
outline:0;
font-size:100%;
vertical-align:baseline;
background:transparent;
}
body {
line-height:1;
}
article,aside,details,figcaption,figure,
footer,header,hgroup,menu,nav,section {
display:block;
}
nav ul {
list-style:none;
}
blockquote, q {
quotes:none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content:'';
content:none;
}
a {
margin:0;
padding:0;
font-size:100%;
vertical-align:baseline;
background:transparent;
}
/* change colours to suit your needs */
ins {
background-color:#ff9;
color:#000;
text-decoration:none;
}
/* change colours to suit your needs */
mark {
background-color:#ff9;
color:#000;
font-style:italic;
font-weight:bold;
}
del {
text-decoration: line-through;
}
abbr[title], dfn[title] {
border-bottom:1px dotted;
cursor:help;
}
table {
border-collapse:collapse;
border-spacing:0;
}
/* change border colour to suit your needs */
hr {
display:block;
height:1px;
border:0;
border-top:1px solid #cccccc;
margin:1em 0;
padding:0;
}
input, select {
vertical-align:middle;
}

View File

@ -1,18 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no">
<style>
undivert(style.css)
</style>
<script>
var bootstrapUrl = "/cgi-bin/nodeinfo";
undivert(app.js)
</script>
</head>
<body>
<noscript>Bitte Javascript aktivieren.</noscript>
</body>
</html>

View File

@ -1,157 +0,0 @@
"use strict"
define([ "lib/gui/nodeinfo"
, "lib/gui/statistics"
, "lib/gui/neighbours"
, "lib/gui/menu"
, "lib/streams"
, "lib/neighbourstream"
], function ( NodeInfo
, Statistics
, Neighbours
, Menu
, Streams
, NeighbourStream
) {
function VerticalSplit(parent) {
var el = document.createElement("div")
el.className = "vertical-split"
parent.appendChild(el)
el.push = function (child) {
var header = document.createElement("h2")
header.appendChild(child.title)
var div = document.createElement("div")
div.className = "frame"
div.node = child
div.appendChild(header)
el.appendChild(div)
child.render(div)
return function () {
div.node.destroy()
el.removeChild(div)
}
}
el.clear = function () {
while (el.firstChild) {
el.firstChild.node.destroy()
el.removeChild(el.firstChild)
}
}
return el
}
var h1
return function (mgmtBus, nodesBus) {
function setTitle(node, state) {
var title = node ? node.hostname : "(not connected)"
document.title = title
h1.textContent = title
var icon = document.createElement("i")
icon.className = "icon-down-dir"
h1.appendChild(icon)
switch (state) {
case "connect":
stateIcon.className = "icon-arrows-cw animate-spin"
break
case "fail":
stateIcon.className = "icon-attention"
break
default:
stateIcon.className = ""
break
}
}
var nodes = []
function nodeMenu() {
var myNodes = nodes.slice()
myNodes.sort(function (a, b) {
a = a.hostname
b = b.hostname
return (a < b) ? -1 : (a > b)
})
var menu = myNodes.map(function (d) {
return [d.hostname, function () {
mgmtBus.pushEvent("goto", d)
}]
})
new Menu(menu).apply(this)
}
var header = document.createElement("header")
h1 = document.createElement("h1")
header.appendChild(h1)
h1.onclick = nodeMenu
var icons = document.createElement("p")
icons.className = "icons"
header.appendChild(icons)
var stateIcon = document.createElement("i")
icons.appendChild(stateIcon)
document.body.appendChild(header)
var container = document.createElement("div")
container.className = "container"
document.body.appendChild(container)
setTitle()
var content = new VerticalSplit(container)
function nodeChanged(nodeInfo) {
setTitle(nodeInfo, "connect")
content.clear()
content.push(new NodeInfo(nodeInfo))
}
function nodeNotArrived(nodeInfo) {
setTitle(nodeInfo, "fail")
}
function nodeArrived(nodeInfo, ip) {
setTitle(nodeInfo)
var neighbourStream = new NeighbourStream(mgmtBus, nodesBus, ip)
var statisticsStream = new Streams.Statistics(ip)
content.push(new Statistics(statisticsStream))
content.push(new Neighbours(nodeInfo, neighbourStream, mgmtBus))
}
function newNodes(d) {
nodes = []
for (var nodeId in d)
nodes.push(d[nodeId])
}
mgmtBus.onEvent({ "goto": nodeChanged
, "arrived": nodeArrived
, "gotoFailed": nodeNotArrived
})
nodesBus.map(".nodes").onValue(newNodes)
return this
}
})

View File

@ -1,39 +0,0 @@
"use strict"
define(function () {
return function (menu) {
return function () {
var background = document.createElement("div")
background.className = "menu-background"
document.body.appendChild(background)
document.body.classList.add("noscroll")
var offset = this.getBoundingClientRect()
var container = document.createElement("ul")
container.className = "menu"
container.style.top = offset.top + "px"
container.style.left = offset.left + "px"
background.onclick = destroy
menu.forEach(function (item) {
var li = document.createElement("li")
li.textContent = item[0]
li.action = item[1]
li.onclick = function () {
destroy()
this.action()
}
container.appendChild(li)
})
document.body.appendChild(container)
function destroy() {
document.body.classList.remove("noscroll")
document.body.removeChild(background)
document.body.removeChild(container)
}
}
}
})

View File

@ -1,274 +0,0 @@
"use strict"
define([ "lib/helper", "lib/gui/signalgraph", "lib/gui/signal"],
function (Helper, SignalGraph, Signal) {
var graphColors = ["#396AB1", "#DA7C30", "#3E9651", "#CC2529", "#535154", "#6B4C9A", "#922428", "#948B3D"]
//graphColors = ["#7293CB", "#E1974C", "#84BA5B", "#D35E60", "#808585", "#9067A7", "#AB6857", "#CCC210"];
var inactiveTime = 200
function SignalEntry(graph, color, stream) {
var signal = new Signal(color)
var remove = graph.add(signal)
var unsubscribe = stream.onValue(update)
this.destroy = function () {
unsubscribe()
remove()
}
this.getSignal = function () {
return signal
}
return this
function update(d) {
if ("wifi" in d)
signal.set(d.wifi.inactive > inactiveTime ? null : d.wifi.signal)
}
}
function TableEntry(parent, nodeInfo, color, stream, mgmtBus, signal) {
var el = parent.insertRow()
var tdHostname = el.insertCell()
var tdTQ = el.insertCell()
var tdSignal = el.insertCell()
var tdDistance = el.insertCell()
var tdInactive = el.insertCell()
var marker = document.createElement("span")
marker.textContent = "⬤ "
marker.style.color = color
tdHostname.appendChild(marker)
var hostname = document.createElement("span")
tdHostname.appendChild(hostname)
var infoSet = false
var unsubscribe = stream.onValue(update)
el.onmouseenter = function () {
el.classList.add("highlight")
signal.setHighlight(true)
}
el.onmouseleave = function () {
el.classList.remove("highlight")
signal.setHighlight(false)
}
el.destroy = function () {
unsubscribe()
parent.tBodies[0].removeChild(el)
}
return el
function update(d) {
if ("wifi" in d) {
var signal = d.wifi.signal
var inactive = d.wifi.inactive
el.classList.toggle("inactive", inactive > inactiveTime)
tdSignal.textContent = signal
tdInactive.textContent = Math.round(inactive / 1000) + " s"
}
if ("batadv" in d)
tdTQ.textContent = Math.round(d.batadv.tq / 2.55) + " %"
else
tdTQ.textContent = ""
if (infoSet)
return
if ("nodeInfo" in d) {
infoSet = true
var link = document.createElement("a")
link.textContent = d.nodeInfo.hostname
link.href = "#"
link.nodeInfo = d.nodeInfo
link.onclick = function () {
mgmtBus.pushEvent("goto", this.nodeInfo)
return false
}
while (hostname.firstChild)
hostname.removeChild(hostname.firstChild)
hostname.appendChild(link)
try {
var distance = Helper.haversine(nodeInfo.location.latitude, nodeInfo.location.longitude,
d.nodeInfo.location.latitude, d.nodeInfo.location.longitude)
tdDistance.textContent = Math.round(distance * 1000) + " m"
} catch (e) {
tdDistance.textContent = ""
}
} else
hostname.textContent = d.id
}
}
function Interface(parent, nodeInfo, iface, stream, mgmtBus) {
var colors = graphColors.slice(0)
var el = document.createElement("div")
el.ifname = iface
parent.appendChild(el)
var h = document.createElement("h3")
h.textContent = iface
el.appendChild(h)
var table = document.createElement("table")
var tr = table.insertRow()
table.classList.add("datatable")
var th = document.createElement("th")
th.textContent = Helper._("Node")
tr.appendChild(th)
th = document.createElement("th")
th.textContent = "TQ"
tr.appendChild(th)
th = document.createElement("th")
th.textContent = "dBm"
tr.appendChild(th)
th = document.createElement("th")
th.textContent = Helper._("Distance")
tr.appendChild(th)
th = document.createElement("th")
th.textContent = Helper._("Inactive")
tr.appendChild(th)
el.appendChild(table)
var wrapper = document.createElement("div")
wrapper.className = "signalgraph"
el.appendChild(wrapper)
var canvas = document.createElement("canvas")
canvas.className = "signal-history"
canvas.height = 200
wrapper.appendChild(canvas)
var graph = new SignalGraph(canvas, -100, 0, true)
var stopStream = stream.skipDuplicates(sameKeys).onValue(update)
var managedNeighbours = {}
function update(d) {
var notUpdated = new Set()
var id
for (id in managedNeighbours)
notUpdated.add(id)
for (id in d) {
if (!(id in managedNeighbours)) {
var neighbourStream = stream.map("." + id).filter( function (d) { return d !== undefined })
var color = colors.shift()
var signal = new SignalEntry(graph, color, neighbourStream)
managedNeighbours[id] = { views: [ signal,
new TableEntry(table, nodeInfo, color, neighbourStream, mgmtBus, signal.getSignal())
],
color: color
}
}
notUpdated.delete(id)
}
notUpdated.forEach(function (id) {
managedNeighbours[id].views.forEach( function (d) { d.destroy() })
colors.push(managedNeighbours[id].color)
delete managedNeighbours[id]
})
}
el.destroy = function () {
stopStream()
for (var id in managedNeighbours)
managedNeighbours[id].views.forEach( function (d) { d.destroy() })
el.removeChild(h)
el.removeChild(wrapper)
el.removeChild(table)
}
}
function sameKeys(a, b) {
a = Object.keys(a).sort()
b = Object.keys(b).sort()
return !(a < b || a > b)
}
function getter(k) {
return function(obj) {
return obj[k]
}
}
return function (nodeInfo, stream, mgmtBus) {
var stopStream, div
function render(el) {
div = document.createElement("div")
el.appendChild(div)
stopStream = stream.skipDuplicates(sameKeys).onValue(update)
function update(d) {
var have = {}
var remove = []
if (div.hasChildNodes()) {
var children = div.childNodes
for (var i = 0; i < children.length; i++) {
var a = children[i]
if (a.ifname in d)
have[a.ifname] = true
else {
a.destroy()
remove.push(a)
}
}
}
remove.forEach(function (d) { div.removeChild(d) })
for (var k in d) {
if (!(k in have))
new Interface(div, nodeInfo, k, stream.map(getter(k)), mgmtBus)
}
}
}
function destroy() {
stopStream()
while (div.firstChild) {
div.firstChild.destroy()
div.removeChild(div.firstChild)
}
}
return { title: document.createTextNode(Helper._("Neighbors"))
, render: render
, destroy: destroy
}
}
})

View File

@ -1,65 +0,0 @@
"use strict"
define(["lib/helper"], function (Helper) {
return function (nodeInfo) {
var el = document.createElement("div")
update(nodeInfo)
function dlEntry(dl, dict, key, prettyName, transform) {
var v = Helper.dictGet(dict, key.split("."))
if (v === null)
return
if (transform) {
v = transform(v)
}
var dt = document.createElement("dt")
var dd = document.createElement("dd")
dt.textContent = prettyName
if (v instanceof Array) {
var tn = v.map(function (d) { return document.createTextNode(d) })
tn.forEach(function (node) {
if (dd.hasChildNodes())
dd.appendChild(document.createElement("br"))
dd.appendChild(node)
})
} else
dd.textContent = v
dl.appendChild(dt)
dl.appendChild(dd)
}
function enabledDisabled(v) {
if (v) {
return Helper._("enabled");
}
return Helper._("disabled");
}
function update(nodeInfo) {
var list = document.createElement("dl")
dlEntry(list, nodeInfo, "hostname", Helper._("Node name"))
dlEntry(list, nodeInfo, "owner.contact", Helper._("Contact"))
dlEntry(list, nodeInfo, "hardware.model", Helper._("Model"))
dlEntry(list, nodeInfo, "network.mac", Helper._("Primary MAC"))
dlEntry(list, nodeInfo, "network.addresses", Helper._("IP Address"))
dlEntry(list, nodeInfo, "software.firmware.release", Helper._("Firmware"))
dlEntry(list, nodeInfo, "software.fastd.enabled", Helper._("Mesh VPN"), enabledDisabled)
dlEntry(list, nodeInfo, "software.autoupdater.enabled", Helper._("Automatic updates"), enabledDisabled)
dlEntry(list, nodeInfo, "software.autoupdater.branch", Helper._("Branch"))
el.appendChild(list)
}
return { title: document.createTextNode(Helper._("Overview"))
, render: function (d) { d.appendChild(el) }
, destroy: function () {}
}
}
})

View File

@ -1,48 +0,0 @@
"use strict"
define(function () {
return function (color) {
var canvas = document.createElement("canvas")
var ctx = canvas.getContext("2d")
var v = null
var radius = 1.2
var highlight = false
function drawPixel(x, y) {
ctx.beginPath()
ctx.fillStyle = color
ctx.arc(x, y, radius, 0, Math.PI * 2, false)
ctx.closePath()
ctx.fill()
}
this.resize = function (w, h) {
canvas.width = w
canvas.height = h
}
this.draw = function (x, scale) {
var y = scale(v)
ctx.clearRect(x, 0, 5, canvas.height)
if (y)
drawPixel(x, y)
}
this.canvas = canvas
this.set = function (d) {
v = d
}
this.setHighlight = function (d) {
highlight = d
}
this.getHighlight = function () {
return highlight
}
return this
}
})

View File

@ -1,137 +0,0 @@
"use strict"
define(function () {
return function (canvas, min, max) {
var i = 0
var graphWidth
var last = 0
var signals = []
var ctx = canvas.getContext("2d")
resize()
window.addEventListener("resize", resize, false)
window.requestAnimationFrame(step)
function step(timestamp) {
var delta = timestamp - last
if (delta > 40) {
draw()
last = timestamp
}
window.requestAnimationFrame(step)
}
function drawGrid() {
var gridctx = ctx
var nLines = Math.floor(canvas.height / 40)
gridctx.save()
gridctx.lineWidth = 0.5
gridctx.strokeStyle = "rgba(0, 0, 0, 0.25)"
gridctx.fillStyle = "rgba(0, 0, 0, 0.5)"
gridctx.textAlign = "end"
gridctx.textBaseline = "bottom"
gridctx.beginPath()
for (var i = 0; i < nLines; i++) {
var y = canvas.height - i * 40
gridctx.moveTo(0, y - 0.5)
gridctx.lineTo(canvas.width, y - 0.5)
var dBm = Math.round(scaleInverse(y, min, max, canvas.height)) + " dBm"
gridctx.save()
gridctx.strokeStyle = "rgba(255, 255, 255, 0.9)"
gridctx.lineWidth = 4
gridctx.miterLimit = 2
gridctx.strokeText(dBm, canvas.width - 5, y - 2.5)
gridctx.fillText(dBm, canvas.width - 5, y - 2.5)
gridctx.restore()
}
gridctx.stroke()
gridctx.strokeStyle = "rgba(0, 0, 0, 0.83)"
gridctx.lineWidth = 1.5
gridctx.strokeRect(0.5, 0.5, canvas.width - 1, canvas.height - 1)
gridctx.restore()
}
function draw() {
var anyHighlight = signals.some( function (d) { return d.getHighlight() })
signals.forEach( function (d) {
d.draw(i, function (v) {
return scale(v, min, max, canvas.height)
})
})
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.save()
signals.forEach( function (d) {
if (anyHighlight)
ctx.globalAlpha = 0.1
if (d.getHighlight())
ctx.globalAlpha = 1
ctx.drawImage(d.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()
i = (i + 1) % graphWidth
}
function scaleInverse(n, min, max, height) {
return (min * n + max * height - max * n) / height
}
function scale(n, min, max, height) {
return (1 - (n - min) / (max - min)) * height
}
function resize() {
var newWidth = canvas.parentNode.clientWidth
if (newWidth === 0 || newWidth === canvas.width)
return
var lastImage = ctx.getImageData(0, 0, newWidth, canvas.height)
canvas.width = newWidth
graphWidth = canvas.width
ctx.putImageData(lastImage, 0, 0)
signals.forEach( function (d) {
d.resize(canvas.width, canvas.height)
})
}
this.add = function (d) {
signals.push(d)
d.resize(canvas.width, canvas.height)
return function () {
signals = signals.filter( function (e) { return e !== d } )
}
}
return this
}
})

View File

@ -1,282 +0,0 @@
"use strict"
define(["lib/helper"], function (Helper) {
function streamElement(type, stream) {
var el = document.createElement(type)
el.destroy = stream.onValue(update)
function update(d) {
el.textContent = d
}
return el
}
function streamNode(stream) {
var el = document.createTextNode("")
el.destroy = stream.onValue(update)
function update(d) {
el.textContent = d
}
return el
}
function mkRow(table, label, stream, sorted) {
var i = -1
if (sorted) {
for (i = 0; i < table.rows.length; i++) {
if (label < table.rows[i].firstChild.textContent)
break
}
}
var tr = table.insertRow(i)
var th = document.createElement("th")
var td = streamElement("td", stream)
th.textContent = label
tr.appendChild(th)
tr.appendChild(td)
tr.destroy = function () {
td.destroy()
table.tBodies[0].removeChild(tr)
}
return tr
}
function mkTrafficRow(table, children, label, stream, selector) {
var tr = table.insertRow()
var th = document.createElement("th")
th.textContent = label
tr.appendChild(th)
var td = tr.insertCell()
var traffic = stream.slidingWindow(2, 2)
var pkts = streamNode(traffic.map(deltaUptime(selector + ".packets")).map(prettyPackets))
var bw = streamNode(traffic.map(deltaUptime(selector + ".bytes")).map(prettyBits))
var bytes = streamNode(stream.map(selector).map(".bytes").map(prettyBytes))
td.appendChild(pkts)
td.appendChild(document.createElement("br"))
td.appendChild(bw)
td.appendChild(document.createElement("br"))
td.appendChild(bytes)
children.push(pkts)
children.push(bw)
children.push(bytes)
}
function mkMeshVPN(el, stream) {
var children = {}
var init = false
var h = document.createElement("h3")
h.textContent = "Mesh-VPN"
var table = document.createElement("table")
var unsubscribe = stream.onValue( function (d) {
function addPeer(peer, path) {
return { peer: peer, path: path }
}
function addPeers(d, path) {
if (!("peers" in d))
return []
var peers = []
for (var peer in d.peers)
peers.push(addPeer(peer, path + ".peers." + peer))
return peers
}
function addGroup(d, path) {
var peers = []
peers = peers.concat(addPeers(d, path))
if ("groups" in d)
for (var group in d.groups)
peers = peers.concat(addGroup(d.groups[group], path + ".groups." + group))
return peers
}
if (d === undefined)
clear()
else {
if (!init) {
init = true
el.appendChild(h)
el.appendChild(table)
}
var peers = addGroup(d, "")
var paths = new Set(peers.map(function (d) { return d.path } ))
for (var path in children)
if (!paths.has(path)) {
children[path].destroy()
delete children[path]
}
peers.forEach( function (peer) {
if (!(peer.path in children))
children[peer.path] = mkRow(table, peer.peer,
stream.startWith(d)
.map(peer.path)
.filter(function (d) { return d !== undefined })
.map(prettyPeer), true)
})
}
})
function clear() {
if (init) {
init = false
el.removeChild(h)
el.removeChild(table)
}
for (var peer in children)
children[peer].destroy()
children = {}
}
function destroy() {
unsubscribe()
clear()
}
return { destroy: destroy }
}
function deltaUptime(selector) {
return function (d) {
var deltaTime = d[1].uptime - d[0].uptime
var d0 = Helper.dictGet(d[0], selector.split(".").splice(1))
var d1 = Helper.dictGet(d[1], selector.split(".").splice(1))
return (d1 - d0) / deltaTime
}
}
function prettyPeer(d) {
if (d === null)
return Helper._("not connected")
else
return Helper._("connected") + " (" + prettyUptime(d.established) + ")"
}
function prettyPackets(d) {
var v = Helper.formatNumberFixed(d, 0)
return v + " "+ Helper._("Packets/s")
}
function prettyPrefix(prefixes, step, d) {
var prefix = 0
while (d > step && prefix < prefixes.length - 1) {
d /= step
prefix++
}
d = Helper.formatNumber(d, 3)
return d + " " + prefixes[prefix]
}
function prettySize(d) {
return prettyPrefix([ "", "k", "M", "G", "T" ], 1024, d)
}
function prettyBits(d) {
return prettySize(d * 8) + "bps"
}
function prettyBytes(d) {
return prettySize(d) + "B"
}
function prettyUptime(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 " + Helper._("Day") + ", "
else if (days > 1)
out += days + " " + Helper._("Days") + ", "
out += hours + ":"
if (minutes < 10)
out += "0"
out += minutes
return out
}
function prettyNVRAM(usage) {
return Helper.formatNumber(usage * 100, 3) + "% " + Helper._("used")
}
function prettyLoad(load) {
return Helper.formatNumberFixed(load, 2)
}
function prettyRAM(memory) {
var usage = 1 - (memory.free + memory.buffers + memory.cached) / memory.total
return prettyNVRAM(usage)
}
return function (stream) {
var children = []
var el = document.createElement("div")
var table = document.createElement("table")
children.push(mkRow(table, Helper._("Uptime"), stream.map(".uptime").map(prettyUptime)))
children.push(mkRow(table, Helper._("Load average"), stream.map(".loadavg").map(prettyLoad)))
children.push(mkRow(table, "RAM", stream.map(".memory").map(prettyRAM)))
children.push(mkRow(table, "NVRAM", stream.map(".rootfs_usage").map(prettyNVRAM)))
children.push(mkRow(table, Helper._("Gateway"), stream.map(".gateway")))
children.push(mkRow(table, Helper._("Clients"), stream.map(".clients.total")))
el.appendChild(table)
var h = document.createElement("h3")
h.textContent = Helper._("Traffic")
el.appendChild(h)
table = document.createElement("table")
mkTrafficRow(table, children, Helper._("Transmitted"), stream, ".traffic.tx")
mkTrafficRow(table, children, Helper._("Received"), stream, ".traffic.rx")
mkTrafficRow(table, children, Helper._("Forwarded"), stream, ".traffic.forward")
el.appendChild(table)
children.push(mkMeshVPN(el, stream.map(".mesh_vpn")))
function destroy() {
children.forEach(function (d) {d.destroy()})
}
return { title: document.createTextNode(Helper._("Statistic"))
, render: function (d) { d.appendChild(el) }
, destroy: destroy
}
}
})

View File

@ -1,170 +0,0 @@
"use strict"
define([ "bacon" ], function (Bacon) {
function get(url) {
return Bacon.fromBinder(function(sink) {
var req = new XMLHttpRequest()
req.open("GET", url)
req.onload = function() {
if (req.status === 200)
sink(new Bacon.Next(req.response))
else
sink(new Bacon.Error(req.statusText))
sink(new Bacon.End())
}
req.onerror = function() {
sink(new Bacon.Error("network error"))
sink(new Bacon.End())
}
req.send()
return function () {}
})
}
function getJSON(url) {
return get(url).map(JSON.parse)
}
function buildUrl(ip, object, param) {
var url = "http://[" + ip + "]/cgi-bin/" + object
if (param) url += "?" + param
return url
}
function request(ip, object, param) {
return getJSON(buildUrl(ip, object, param))
}
function dictGet(dict, key) {
var k = key.shift()
if (!(k in dict))
return null
if (key.length === 0)
return dict[k]
return dictGet(dict[k], key)
}
function localizeNumber(d) {
var sep = ','
return d.replace('.', sep)
}
function formatNumberFixed(d, digits) {
return localizeNumber(d.toFixed(digits))
}
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 haversine() {
var radians = Array.prototype.map.call(arguments, function(deg) { return deg / 180.0 * Math.PI })
var lat1 = radians[0], lon1 = radians[1], lat2 = radians[2], lon2 = radians[3]
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
}
function _(s) {
var i, lang, langs, dict = {
"de": {
"Node": "Knoten",
"Distance": "Entfernung",
"Inactive": "Inaktiv",
"Node name": "Knotenname",
"Contact": "Kontakt",
"Model": "Modell",
"Primary MAC": "Primäre MAC",
"IP Address": "IP-Adresse",
"Automatic updates": "Automatische Updates",
"Overview": "Übersicht",
"used": "belegt",
"Uptime": "Laufzeit",
"Load average": "Systemlast",
"Transmitted": "Gesendet",
"Received": "Empfangen",
"Forwarded": "Weitergeleitet",
"Day": "Tag",
"Days": "Tage",
"connected": "verbunden",
"not connected": "nicht verbunden",
"Packets/s": "Pakete/s",
"Statistic": "Statistik",
"Neighbors": "Nachbarknoten",
"Mesh VPN": "Mesh-VPN",
"enabled": "aktiviert",
"disabled": "deaktiviert"
},
"ru": {
"Node": "Узел",
"Distance": "Дальность",
"Inactive": "Не активен",
"Node name": "Имя узла",
"Contact": "Контакт",
"Model": "Модель",
"Primary MAC": "Основной MAC",
"IP Address": "IP Адрес",
"Automatic updates": "Автоматические обновления",
"Overview": "Обзор",
"used": "используется",
"Uptime": "Время работы",
"Load average": "Загрузка системы",
"Gateway": "Шлюз",
"Clients": "Клиенты",
"Transmitted": "Передано",
"Received": "Получено",
"Forwarded": "Переправленно",
"Day": "День",
"Days": "Дней",
"connected": "подключено",
"not connected": "не подключено",
"Packets/s": "Пакетов/c",
"Statistic": "Статистика",
"Traffic": "Трафик",
"Neighbors": "Соседи",
"Firmware": "Прошивка",
"Branch": "Ветка"
}
}
if (navigator.languages)
langs = navigator.languages
else if (navigator.language)
langs = [navigator.language]
else
langs = []
for (i=0; i<langs.length; i++) {
lang = langs[i].split('-')[0]
if (lang == "en")
return s
else if (lang in dict && s in dict[lang])
return dict[lang][s]
}
return s
}
return { buildUrl: buildUrl
, request: request
, getJSON: getJSON
, dictGet: dictGet
, formatNumber: formatNumber
, formatNumberFixed: formatNumberFixed
, haversine: haversine
, _: _
}
})

View File

@ -1,132 +0,0 @@
"use strict"
define([ "bacon"
, "lib/helper"
, "lib/streams"
], function(Bacon, Helper, Streams) {
return function (mgmtBus, nodesBus, ip) {
function nodeQuerier() {
var asked = {}
var timeout = 6000
return function (ifname) {
var now = new Date().getTime()
if (ifname in asked && now - asked[ifname] < timeout)
return Bacon.never()
asked[ifname] = now
return Streams.nodeInfo(ip, ifname).map(function (d) {
return { "ifname": ifname
, "nodeInfo": d
}
})
}
}
var querierAsk = new Bacon.Bus()
var querier = querierAsk.flatMap(nodeQuerier())
querier.map(".nodeInfo").onValue(mgmtBus, "pushEvent", "nodeinfo")
function wrapIfname(ifname, d) {
return [ifname, d]
}
function extractIfname(d) {
var r = {}
for (var station in d) {
var ifname = d[station].ifname
delete d[station].ifname
if (!(ifname in r))
r[ifname] = {}
r[ifname][station] = d[station]
}
return r
}
function stationsStream(ifname) {
return new Streams.Stations(ip, ifname).map(wrapIfname, ifname)
}
function magic(interfaces) {
var ifnames = Object.keys(interfaces)
ifnames.forEach(querierAsk.push)
var wifiStream = Bacon.fromArray(ifnames)
.flatMap(stationsStream)
.scan({}, function (a, b) {
a[b[0]] = b[1]
return a
})
var batadvStream = new Streams.Batadv(ip).toProperty({})
return Bacon.combineWith(combine, wifiStream
, batadvStream.map(extractIfname)
, nodesBus.map(".macs")
)
}
function combine(wifi, batadv, macs) {
var interfaces = combineWithIfnames(wifi, batadv)
for (var ifname in interfaces) {
var stations = interfaces[ifname]
for (var station in stations) {
stations[station].id = station
if (station in macs)
stations[station].nodeInfo = macs[station]
else
querierAsk.push(ifname)
}
}
return interfaces
}
function combineWithIfnames(wifi, batadv) {
var ifnames = Object.keys(wifi).concat(Object.keys(batadv))
// remove duplicates
ifnames.filter(function(e, i) {
return ifnames.indexOf(e) === i
})
var out = {}
ifnames.forEach(function (ifname) {
out[ifname] = combineWifiBatadv(wifi[ifname], batadv[ifname])
})
return out
}
function combineWifiBatadv(wifi, batadv) {
var station
var out = {}
for (station in batadv) {
if (!(station in out))
out[station] = {}
out[station].batadv = batadv[station]
}
for (station in wifi) {
if (!(station in out))
out[station] = {}
out[station].wifi = wifi[station]
}
return out
}
return Helper.request(ip, "interfaces").flatMap(magic)
}
})

View File

@ -1,66 +0,0 @@
"use strict"
define(["bacon", "lib/helper"], function(Bacon, Helper) {
function nodeInfo(ip, ifname) {
return Bacon.fromBinder(function (sink) {
var url = Helper.buildUrl(ip, "dyn/neighbours-nodeinfo", ifname)
var evtSource = new EventSource(url)
evtSource.addEventListener("neighbour", function(e) {
var r = sink(new Bacon.Next(JSON.parse(e.data)))
if (r === Bacon.noMore)
tearDown()
}, false)
evtSource.addEventListener("eot", function() {
evtSource.close()
sink(new Bacon.End())
}, false)
function tearDown() {
evtSource.close()
}
return tearDown
})
}
function simpleStream(url) {
return Bacon.fromBinder(function (sink) {
var evtSource = new EventSource(url)
evtSource.onmessage = function (e) {
var r = sink(new Bacon.Next(JSON.parse(e.data)))
if (r === Bacon.noMore)
tearDown()
}
function tearDown() {
evtSource.close()
}
return tearDown
})
}
function batadv(ip) {
var url = Helper.buildUrl(ip, "dyn/neighbours-batadv")
return simpleStream(url)
}
function stations(ip, ifname) {
var url = Helper.buildUrl(ip, "dyn/stations", ifname)
return simpleStream(url)
}
function statistics(ip) {
var url = Helper.buildUrl(ip, "dyn/statistics")
return simpleStream(url).skipDuplicates(function (a, b) {return (a.uptime === b.uptime)})
}
return { nodeInfo: nodeInfo
, Batadv: batadv
, Stations: stations
, Statistics: statistics
}
})

View File

@ -1,119 +0,0 @@
"use strict"
require([ "bacon"
, "lib/helper"
, "lib/streams"
, "lib/gui"
], function(Bacon, Helper, Streams, GUI) {
var mgmtBus = new Bacon.Bus()
mgmtBus.pushEvent = function (key, a) {
var v = [key].concat(a)
return this.push(v)
}
mgmtBus.onEvent = function (events) {
return this.onValue(function (e) {
var d = e.slice() // shallow copy so calling shift doesn't change it
var ev = d.shift()
if (ev in events)
events[ev].apply(this, d)
})
}
var nodesBusIn = new Bacon.Bus()
var nodesBus = nodesBusIn.scan({ "nodes": {}
, "macs": {}
}, scanNodeInfo)
new GUI(mgmtBus, nodesBus)
mgmtBus.onEvent({ "goto": gotoNode
, "nodeinfo": function (d) { nodesBusIn.push(d) }
})
function tryIp(ip) {
return Helper.request(ip, "nodeinfo").map(function () { return ip })
}
var gotoEpoch = 0
function onEpoch(epoch, f) {
return function (d) {
if (epoch === gotoEpoch)
return f(d)
}
}
function gotoNode(nodeInfo) {
gotoEpoch++
var addresses = nodeInfo.network.addresses.filter(function (d) { return !/^fe80:/.test(d) })
var race = Bacon.fromArray(addresses).flatMap(tryIp).withStateMachine([], function (acc, ev) {
if (ev.isError())
return [acc.concat(ev.error), []]
else if (ev.isEnd() && acc.length > 0)
return [undefined, [new Bacon.Error(acc), ev]]
else if (ev.hasValue())
return [[], [ev, new Bacon.End()]]
})
race.onValue(onEpoch(gotoEpoch, function (d) {
mgmtBus.pushEvent("arrived", [nodeInfo, d])
}))
race.onError(onEpoch(gotoEpoch, function () {
mgmtBus.pushEvent("gotoFailed", nodeInfo)
}))
}
function scanNodeInfo(a, nodeInfo) {
a.nodes[nodeInfo.node_id] = nodeInfo
var mesh = Helper.dictGet(nodeInfo, ["network", "mesh"])
if (mesh)
for (var m in mesh)
for (var ifname in mesh[m].interfaces)
mesh[m].interfaces[ifname].forEach( function (d) {
a.macs[d] = nodeInfo
})
return a
}
var lsavailable = false
try {
localStorage.setItem("t", "t")
localStorage.removeItem("t")
lsavailable = true
} catch(e) {
lsavailable = false
}
if ( lsavailable && localStorage.nodes)
JSON.parse(localStorage.nodes).forEach(nodesBusIn.push)
nodesBus.map(".nodes").onValue(function (nodes) {
var out = []
for (var k in nodes)
out.push(nodes[k])
if (lsavailable)
localStorage.nodes = JSON.stringify(out)
})
var bootstrap = Helper.getJSON(bootstrapUrl)
bootstrap.onError(function () {
console.log("FIXME bootstrapping failed")
})
bootstrap.onValue(function (d) {
mgmtBus.pushEvent("nodeinfo", d)
mgmtBus.pushEvent("goto", d)
})
})