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:
parent
b1aa5390a7
commit
88906f238b
@ -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))
|
||||||
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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
113
package/gluon-status-page/i18n/de.po
Normal file
113
package/gluon-status-page/i18n/de.po
Normal 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"
|
104
package/gluon-status-page/i18n/gluon-status-page.pot
Normal file
104
package/gluon-status-page/i18n/gluon-status-page.pot
Normal 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 ""
|
31
package/gluon-status-page/i18n/ru.README
Normal file
31
package/gluon-status-page/i18n/ru.README
Normal 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": "Ветка"
|
@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
737
package/gluon-status-page/javascript/status-page.js
Normal file
737
package/gluon-status-page/javascript/status-page.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
@ -0,0 +1,3 @@
|
|||||||
|
entry({}, call(function(http, renderer)
|
||||||
|
renderer.render('status-page', nil, 'gluon-status-page')
|
||||||
|
end))
|
@ -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
|
||||||
|
}
|
195
package/gluon-status-page/sass/status-page.scss
Normal file
195
package/gluon-status-page/sass/status-page.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +0,0 @@
|
|||||||
({
|
|
||||||
paths: {
|
|
||||||
"bacon": "../Bacon"
|
|
||||||
},
|
|
||||||
baseUrl: "js/",
|
|
||||||
name: "../almond",
|
|
||||||
include: "main",
|
|
||||||
optimize: "uglify2",
|
|
||||||
out: "app.js",
|
|
||||||
})
|
|
@ -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
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
@ -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>
|
|
@ -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
|
|
||||||
}
|
|
||||||
})
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
@ -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 () {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
@ -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
|
|
||||||
}
|
|
||||||
})
|
|
@ -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
|
|
||||||
}
|
|
||||||
})
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
@ -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
|
|
||||||
, _: _
|
|
||||||
}
|
|
||||||
})
|
|
@ -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)
|
|
||||||
}
|
|
||||||
})
|
|
@ -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
|
|
||||||
}
|
|
||||||
})
|
|
@ -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)
|
|
||||||
})
|
|
||||||
})
|
|
Loading…
Reference in New Issue
Block a user