From 88906f238bad82a298968d5bddcbcf56a680fb77 Mon Sep 17 00:00:00 2001
From: Matthias Schiffer <mschiffer@universe-factory.net>
Date: Mon, 26 Feb 2018 01:35:11 +0100
Subject: [PATCH] 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
---
 package/gluon-status-page/Makefile            |  70 +-
 .../lib/gluon/status-page/view/layout.html    |  24 +
 .../gluon/status-page/view/status-page.html   | 162 ++++
 .../lib/gluon/status-page/www/index.html      |   8 +
 .../status-page/www/static/status-page.css    |   1 +
 .../status-page/www/static/status-page.js     |   1 +
 package/gluon-status-page/i18n/de.po          | 113 +++
 .../i18n/gluon-status-page.pot                | 104 +++
 package/gluon-status-page/i18n/ru.README      |  31 +
 .../gluon-status-page/iconfont-config.json    | 100 ---
 .../javascript/status-page.js                 | 737 ++++++++++++++++++
 .../status-page/controller/status-page.lua    |   3 +
 .../lib/gluon/status-page/www/cgi-bin/status  |   8 +
 .../gluon-status-page/sass/status-page.scss   | 195 +++++
 package/gluon-status-page/src/build.js        |  10 -
 .../gluon-status-page/src/css/animation.css   |  12 -
 package/gluon-status-page/src/css/font.css    |  53 --
 package/gluon-status-page/src/css/main.css    | 171 ----
 package/gluon-status-page/src/css/menu.css    |  50 --
 package/gluon-status-page/src/css/reset.css   |  86 --
 package/gluon-status-page/src/index.html.m4   |  18 -
 package/gluon-status-page/src/js/lib/gui.js   | 157 ----
 .../gluon-status-page/src/js/lib/gui/menu.js  |  39 -
 .../src/js/lib/gui/neighbours.js              | 274 -------
 .../src/js/lib/gui/nodeinfo.js                |  65 --
 .../src/js/lib/gui/signal.js                  |  48 --
 .../src/js/lib/gui/signalgraph.js             | 137 ----
 .../src/js/lib/gui/statistics.js              | 282 -------
 .../gluon-status-page/src/js/lib/helper.js    | 170 ----
 .../src/js/lib/neighbourstream.js             | 132 ----
 .../gluon-status-page/src/js/lib/streams.js   |  66 --
 package/gluon-status-page/src/js/main.js      | 119 ---
 32 files changed, 1402 insertions(+), 2044 deletions(-)
 create mode 100644 package/gluon-status-page/files/lib/gluon/status-page/view/layout.html
 create mode 100644 package/gluon-status-page/files/lib/gluon/status-page/view/status-page.html
 create mode 100644 package/gluon-status-page/files/lib/gluon/status-page/www/index.html
 create mode 100644 package/gluon-status-page/files/lib/gluon/status-page/www/static/status-page.css
 create mode 100644 package/gluon-status-page/files/lib/gluon/status-page/www/static/status-page.js
 create mode 100644 package/gluon-status-page/i18n/de.po
 create mode 100644 package/gluon-status-page/i18n/gluon-status-page.pot
 create mode 100644 package/gluon-status-page/i18n/ru.README
 delete mode 100644 package/gluon-status-page/iconfont-config.json
 create mode 100644 package/gluon-status-page/javascript/status-page.js
 create mode 100644 package/gluon-status-page/luasrc/lib/gluon/status-page/controller/status-page.lua
 create mode 100755 package/gluon-status-page/luasrc/lib/gluon/status-page/www/cgi-bin/status
 create mode 100644 package/gluon-status-page/sass/status-page.scss
 delete mode 100644 package/gluon-status-page/src/build.js
 delete mode 100644 package/gluon-status-page/src/css/animation.css
 delete mode 100644 package/gluon-status-page/src/css/font.css
 delete mode 100644 package/gluon-status-page/src/css/main.css
 delete mode 100644 package/gluon-status-page/src/css/menu.css
 delete mode 100644 package/gluon-status-page/src/css/reset.css
 delete mode 100644 package/gluon-status-page/src/index.html.m4
 delete mode 100644 package/gluon-status-page/src/js/lib/gui.js
 delete mode 100644 package/gluon-status-page/src/js/lib/gui/menu.js
 delete mode 100644 package/gluon-status-page/src/js/lib/gui/neighbours.js
 delete mode 100644 package/gluon-status-page/src/js/lib/gui/nodeinfo.js
 delete mode 100644 package/gluon-status-page/src/js/lib/gui/signal.js
 delete mode 100644 package/gluon-status-page/src/js/lib/gui/signalgraph.js
 delete mode 100644 package/gluon-status-page/src/js/lib/gui/statistics.js
 delete mode 100644 package/gluon-status-page/src/js/lib/helper.js
 delete mode 100644 package/gluon-status-page/src/js/lib/neighbourstream.js
 delete mode 100644 package/gluon-status-page/src/js/lib/streams.js
 delete mode 100644 package/gluon-status-page/src/js/main.js

diff --git a/package/gluon-status-page/Makefile b/package/gluon-status-page/Makefile
index 7b496dddf..c283f9289 100644
--- a/package/gluon-status-page/Makefile
+++ b/package/gluon-status-page/Makefile
@@ -1,78 +1,38 @@
 include $(TOPDIR)/rules.mk
 
 PKG_NAME:=gluon-status-page
-PKG_VERSION:=2
-PKG_RELEASE:=1
+PKG_VERSION:=3
 
-PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
-PKG_BUILD_DEPENDS:=node/host
+include ../gluon.mk
 
-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
   SECTION:=gluon
   CATEGORY:=Gluon
-  TITLE:=Adds a status page showing information about the node.
-  DEPENDS:=+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.
+  TITLE:=Status page showing information about the node
+  DEPENDS:=+gluon-web +gluon-status-page-api
 endef
 
 define Build/Prepare
 	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
 
 define Build/Compile
-	cd $(PKG_BUILD_DIR) && \
-		node r.js -o build.js && \
-		node r.js -o cssIn=css/main.css out=style.css && \
-		$(M4) index.html.m4 > index.html
+	$(call GluonSrcDiet,./luasrc,$(PKG_BUILD_DIR)/luadest/)
+	$(call GluonBuildI18N,gluon-status-page,i18n)
 endef
 
 define Package/gluon-status-page/install
-	$(INSTALL_DIR) $(1)/lib/gluon/status-page/www/
-	$(INSTALL_DATA) $(PKG_BUILD_DIR)/index.html $(1)/lib/gluon/status-page/www/
+	$(CP) ./files/* $(1)/
+	$(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
 
 $(eval $(call BuildPackage,gluon-status-page))
diff --git a/package/gluon-status-page/files/lib/gluon/status-page/view/layout.html b/package/gluon-status-page/files/lib/gluon/status-page/view/layout.html
new file mode 100644
index 000000000..11969ddc2
--- /dev/null
+++ b/package/gluon-status-page/files/lib/gluon/status-page/view/layout.html
@@ -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>
diff --git a/package/gluon-status-page/files/lib/gluon/status-page/view/status-page.html b/package/gluon-status-page/files/lib/gluon/status-page/view/status-page.html
new file mode 100644
index 000000000..2d023af76
--- /dev/null
+++ b/package/gluon-status-page/files/lib/gluon/status-page/view/status-page.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>
diff --git a/package/gluon-status-page/files/lib/gluon/status-page/www/index.html b/package/gluon-status-page/files/lib/gluon/status-page/www/index.html
new file mode 100644
index 000000000..d5da1c84b
--- /dev/null
+++ b/package/gluon-status-page/files/lib/gluon/status-page/www/index.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>
diff --git a/package/gluon-status-page/files/lib/gluon/status-page/www/static/status-page.css b/package/gluon-status-page/files/lib/gluon/status-page/www/static/status-page.css
new file mode 100644
index 000000000..a6a974e2c
--- /dev/null
+++ b/package/gluon-status-page/files/lib/gluon/status-page/www/static/status-page.css
@@ -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}}
diff --git a/package/gluon-status-page/files/lib/gluon/status-page/www/static/status-page.js b/package/gluon-status-page/files/lib/gluon/status-page/www/static/status-page.js
new file mode 100644
index 000000000..55ee27a64
--- /dev/null
+++ b/package/gluon-status-page/files/lib/gluon/status-page/www/static/status-page.js
@@ -0,0 +1 @@
+"use strict";!function(){var t=JSON.parse(document.body.getAttribute("data-translations"));function e(e,n){return e.toFixed(n).replace(/\./,t["."])}function n(t,n){n--;for(var i=t;i>=10&&n>0;i/=10)n--;return e(t,n)}function i(t){return function(t,e,i){for(var r=0;i>e&&r<t.length-1;)i/=e,r++;return(i=n(i,3))+" "+t[r]}(["","K","M","G","T"],1024,t)}String.prototype.sprintf=function(){var t=0,e=arguments;return this.replace(/%s/g,function(){return e[t++]})};var r={id:function(t){return t},decimal:function(t){return e(t,2)},percent:function(e){return t["%s used"].sprintf(n(100*e,3)+"%")},memory:function(t){var e=1-(t.free+t.buffers+t.cached)/t.total;return r.percent(e)},time:function(e){var n=Math.round(e/60),i=Math.floor(n/1440),r=Math.floor(n%1440/60);n=Math.floor(n%60);var a="";return 1===i?a+=t["1 day"]+", ":i>1&&(a+=t["%s days"].sprintf(i)+", "),a+=r+":",n<10&&(a+="0"),a+=n},packetsDiff:function(n,i,r){if(r>0)return a=(n-i)/r,t["%s packets/s"].sprintf(e(a,0));var a},bytesDiff:function(t,e,n){if(n>0)return i(8*((t-e)/n))+"bps"},bytes:function(t){return i(t)+"B"}};function a(t,e){return e.split("/").forEach(function(e){t&&(t=t[e])}),t}function o(t,e){var n=new EventSource(t),i={};n.onmessage=function(t){var n=JSON.parse(t.data);e(n,i),i=n},n.onerror=function(){n.close(),window.setTimeout(function(){o(t,e)},3e3)}}var c,s=document.body.getAttribute("data-node-address");try{c=JSON.parse(document.body.getAttribute("data-node-location"))}catch(t){}var u=document.querySelectorAll("[data-statistics]");o("/cgi-bin/dyn/statistics",function(e,n){var i=e.uptime-n.uptime;u.forEach(function(t){var o=t.getAttribute("data-statistics"),c=t.getAttribute("data-format"),s=a(n,o),u=a(e,o);try{var l=r[c](u,s,i);void 0!==l&&(t.textContent=l)}catch(t){console.error(t)}});try{!function(e){var n=document.getElementById("mesh-vpn");if(e){n.style.display="";for(var i=document.getElementById("mesh-vpn-peers");i.lastChild;)i.removeChild(i.lastChild);var a=function t(e,n){return Object.keys(n.peers||{}).forEach(function(t){e.push([t,n.peers[t]])}),Object.keys(n.groups||{}).forEach(function(i){t(e,n.groups[i])}),e}([],e);a.sort(),a.forEach(function(e){var n=document.createElement("tr"),a=document.createElement("th");a.textContent=e[0],n.appendChild(a);var o=document.createElement("td");e[1]?o.textContent=t.connected+" ("+r.time(e[1].established)+")":o.textContent=t["not connected"],n.appendChild(o),i.appendChild(n)})}else n.style.display="none"}(e.mesh_vpn)}catch(t){console.error(t)}});var l={};function h(t){var e=document.createElement("canvas"),n=e.getContext("2d"),i=null,r=1.2;return{canvas:e,highlight:!1,resize:function(t,i){try{n.getImageData(0,0,t,i)}catch(t){}e.width=t,e.height=i},draw:function(a,o){var c,s,u=o(i);n.clearRect(a,0,5,e.height),u&&(c=a,s=u,n.beginPath(),n.fillStyle=t,n.arc(c,s,r,0,2*Math.PI,!1),n.closePath(),n.fill())},set:function(t){i=t}}}function f(){var t=-100,e=0,n=0,i=[],r=document.createElement("canvas");r.className="signalgraph",r.height=200;var a=r.getContext("2d");function o(){r.width=r.clientWidth,i.forEach(function(t){t.resize(r.width,r.height)})}function c(){if(0!==r.clientWidth){r.width!==r.clientWidth&&o(),a.clearRect(0,0,r.width,r.height);var c=!1;i.forEach(function(t){t.highlight&&(c=!0)}),a.save(),i.forEach(function(i){c&&(a.globalAlpha=.2),i.highlight&&(a.globalAlpha=1),i.draw(n,function(n){return i=n,a=t,o=e,c=r.height,(1-(i-a)/(o-a))*c;var i,a,o,c}),a.drawImage(i.canvas,0,0)}),a.restore(),a.save(),a.beginPath(),a.strokeStyle="rgba(255, 180, 0, 0.15)",a.lineWidth=5,a.moveTo(n+2.5,0),a.lineTo(n+2.5,r.height),a.stroke(),function(){var n,i,o,c,s=Math.floor(r.height/40);a.save(),a.lineWidth=.5,a.strokeStyle="rgba(0, 0, 0, 0.25)",a.fillStyle="rgba(0, 0, 0, 0.5)",a.textAlign="end",a.textBaseline="bottom",a.beginPath();for(var u=0;u<s;u++){var l=r.height-40*u;a.moveTo(0,l-.5),a.lineTo(r.width,l-.5);var h=Math.round((n=l,i=t,o=e,c=r.height,(i*n+o*(c-n))/c))+" dBm";a.save(),a.strokeStyle="rgba(255, 255, 255, 0.9)",a.lineWidth=4,a.miterLimit=2,a.strokeText(h,r.width-5,l-2.5),a.fillText(h,r.width-5,l-2.5),a.restore()}a.stroke(),a.strokeStyle="rgba(0, 0, 0, 0.83)",a.lineWidth=1.5,a.strokeRect(.5,.5,r.width-1,r.height-1),a.restore()}()}}o(),window.addEventListener("resize",c);var s=0;return window.requestAnimationFrame(function t(e){e-s>40&&(c(),n=(n+1)%r.width,s=e),window.requestAnimationFrame(t)}),{el:r,addSignal:function(t){i.push(t),t.resize(r.width,r.height)},removeSignal:function(t){i.splice(i.indexOf(t),1)}}}function d(t,e,n,i){var r=t.table.insertRow(),a=r.insertCell();if(t.wireless){var o=document.createElement("span");o.textContent="⬤ ",o.style.color=n,a.appendChild(o)}var u=document.createElement("span");u.textContent=e,a.appendChild(u);var l,f,d,g,v,m=r.insertCell();function p(){v&&window.clearTimeout(v),v=window.setTimeout(function(){g&&t.signalgraph.removeSignal(g),r.parentNode.removeChild(r),i()},6e4)}function b(t){var e=function(t){"::"==t.slice(0,2)&&(t="0"+t),"::"==t.slice(-2)&&(t+="0");var e=t.split(":"),n=e.length,i=[];return e.forEach(function(t,e){if(""===t)for(;n++<=8;)i.push(0);else{if(!/^[a-f0-9]{1,4}$/i.test(t))return;i.push(parseInt(t,16))}}),i}(t);if(e){var n="";return e.forEach(function(t){n+=("0000000000000000"+t.toString(2)).slice(-16)}),n}}return m.textContent="-",t.wireless&&((l=r.insertCell()).textContent="-",(f=r.insertCell()).textContent="-",(d=r.insertCell()).textContent="-",g=h(n),t.signalgraph.addSignal(g)),r.onmouseenter=function(){r.classList.add("highlight"),g&&(g.highlight=!0)},r.onmouseleave=function(){r.classList.remove("highlight"),g&&(g.highlight=!1)},p(),{update_nodeinfo:function(t){var e,n,i,r,a,o,l,h,d=function(t){var e=b(s);if(t&&t[0]){(t=t.map(function(t){var n=b(t);if(!n)return[-1];var i=0;return e&&(i=function(t,e){var n;for(n=0;n<t.length&&n<e.length&&t[n]===e[n];n++);return n}(e,n)),[i,n,t]})).sort(function(t,e){return t[0]<e[0]?1:t[0]>e[0]?-1:t[1]<e[1]?-1:t[1]>e[1]?1:0});var n=t[0][2];return n&&!/^fe80:/i.test(n)?n:void 0}}(t.network.addresses);if(d){if("span"===u.nodeName.toLowerCase()){var g=u;u=document.createElement("a"),g.parentNode.replaceChild(u,g)}u.href="http://["+d+"]/"}if(u.textContent=t.hostname,c&&t.location){var v=(e=c.latitude,n=c.longitude,i=t.location.latitude,r=t.location.longitude,a=Math.PI/180,o=(i*=a)-(e*=a),l=(r*=a)-(n*=a),h=Math.sin(o/2)*Math.sin(o/2)+Math.sin(l/2)*Math.sin(l/2)*Math.cos(e)*Math.cos(i),2*Math.asin(Math.sqrt(h))*6372.8);f.textContent=Math.round(1e3*v)+" m"}p()},update_mesh:function(t){m.textContent=Math.round(t.tq/2.55)+" %",p()},update_wifi:function(t){l.textContent=t.signal,d.textContent=Math.round(t.inactive/1e3)+" s",r.classList.toggle("inactive",t.inactive>200),g.set(t.inactive>200?null:t.signal),p()}}}function g(t,e,n){var i,r={};n&&(i=f(),t.appendChild(i.el));var a={table:t.firstElementChild,signalgraph:i,ifname:e,wireless:n},c=!1,s={},u=[];function l(){if(!c){c=!0;var t=new EventSource("/cgi-bin/dyn/neighbours-nodeinfo?"+encodeURIComponent(e));t.addEventListener("neighbour",function(t){try{var e=JSON.parse(t.data);(n=e,i=[],a=n.network.mesh,Object.keys(a).forEach(function(t){var e=a[t].interfaces;Object.keys(e).forEach(function(t){e[t].forEach(function(t){i.push(t)})})}),i).forEach(function(t){var n=r[t];if(n){delete s[t];try{n.update_nodeinfo(e)}catch(t){console.error(t)}}})}catch(t){console.error(t)}var n,i,a},!1),t.onerror=function(){t.close(),c=!1,Object.keys(s).forEach(function(t){s[t]>0&&(s[t]--,l())})}}}function h(t){var e=r[t];return e||(s[t]=3,e=r[t]=d(a,t,(u[0]||(u=["#396AB1","#DA7C30","#3E9651","#CC2529","#535154","#6B4C9A","#922428","#948B3D"]),u.shift()),function(){delete s[t],delete r[t]}),l()),e}return n&&o("/cgi-bin/dyn/stations?"+encodeURIComponent(e),function(t){Object.keys(t).forEach(function(e){var n=t[e];h(e).update_wifi(n)})}),{get_neigh:h}}document.querySelectorAll("[data-interface]").forEach(function(t){var e=t.getAttribute("data-interface"),n=(t.getAttribute("data-interface-address"),!!t.getAttribute("data-interface-wireless"));l[e]=g(t,e,n)}),o("/cgi-bin/dyn/neighbours-batadv",function(t){Object.keys(t).forEach(function(e){var n=t[e],i=l[n.ifname];i&&i.get_neigh(e).update_mesh(n)})})}();
\ No newline at end of file
diff --git a/package/gluon-status-page/i18n/de.po b/package/gluon-status-page/i18n/de.po
new file mode 100644
index 000000000..b6ff1a78f
--- /dev/null
+++ b/package/gluon-status-page/i18n/de.po
@@ -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"
diff --git a/package/gluon-status-page/i18n/gluon-status-page.pot b/package/gluon-status-page/i18n/gluon-status-page.pot
new file mode 100644
index 000000000..b4c3882e6
--- /dev/null
+++ b/package/gluon-status-page/i18n/gluon-status-page.pot
@@ -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 ""
diff --git a/package/gluon-status-page/i18n/ru.README b/package/gluon-status-page/i18n/ru.README
new file mode 100644
index 000000000..3f5101af4
--- /dev/null
+++ b/package/gluon-status-page/i18n/ru.README
@@ -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": "Ветка"
diff --git a/package/gluon-status-page/iconfont-config.json b/package/gluon-status-page/iconfont-config.json
deleted file mode 100644
index af8718cc4..000000000
--- a/package/gluon-status-page/iconfont-config.json
+++ /dev/null
@@ -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"
-    }
-  ]
-}
\ No newline at end of file
diff --git a/package/gluon-status-page/javascript/status-page.js b/package/gluon-status-page/javascript/status-page.js
new file mode 100644
index 000000000..7a890bdc6
--- /dev/null
+++ b/package/gluon-status-page/javascript/status-page.js
@@ -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);
+		});
+	});
+})();
diff --git a/package/gluon-status-page/luasrc/lib/gluon/status-page/controller/status-page.lua b/package/gluon-status-page/luasrc/lib/gluon/status-page/controller/status-page.lua
new file mode 100644
index 000000000..18ea1864c
--- /dev/null
+++ b/package/gluon-status-page/luasrc/lib/gluon/status-page/controller/status-page.lua
@@ -0,0 +1,3 @@
+entry({}, call(function(http, renderer)
+	renderer.render('status-page', nil, 'gluon-status-page')
+end))
diff --git a/package/gluon-status-page/luasrc/lib/gluon/status-page/www/cgi-bin/status b/package/gluon-status-page/luasrc/lib/gluon/status-page/www/cgi-bin/status
new file mode 100755
index 000000000..7e5079ac7
--- /dev/null
+++ b/package/gluon-status-page/luasrc/lib/gluon/status-page/www/cgi-bin/status
@@ -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
+}
diff --git a/package/gluon-status-page/sass/status-page.scss b/package/gluon-status-page/sass/status-page.scss
new file mode 100644
index 000000000..cfaa561fa
--- /dev/null
+++ b/package/gluon-status-page/sass/status-page.scss
@@ -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;
+		}
+	}
+}
diff --git a/package/gluon-status-page/src/build.js b/package/gluon-status-page/src/build.js
deleted file mode 100644
index a1b1d703c..000000000
--- a/package/gluon-status-page/src/build.js
+++ /dev/null
@@ -1,10 +0,0 @@
-({
-  paths: {
-    "bacon": "../Bacon"
-  },
-  baseUrl: "js/",
-  name: "../almond",
-  include: "main",
-  optimize: "uglify2",
-  out: "app.js",
-})
diff --git a/package/gluon-status-page/src/css/animation.css b/package/gluon-status-page/src/css/animation.css
deleted file mode 100644
index e6edbf3c5..000000000
--- a/package/gluon-status-page/src/css/animation.css
+++ /dev/null
@@ -1,12 +0,0 @@
-/*
-   Animation example, for spinners
-*/
-.animate-spin {
-  animation: spin 2s linear infinite;
-  display: inline-block;
-}
-@keyframes spin {
-  100% {
-    transform: rotate(360deg);
-  }
-}
diff --git a/package/gluon-status-page/src/css/font.css b/package/gluon-status-page/src/css/font.css
deleted file mode 100644
index c26ee3559..000000000
--- a/package/gluon-status-page/src/css/font.css
+++ /dev/null
@@ -1,53 +0,0 @@
-@font-face {
-  font-family: 'statuspage';
-  src: url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAABBIABEAAAAAGvAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAABgAAAABwAAAAcbmIpTEdERUYAAAGcAAAAHQAAACAAQAAET1MvMgAAAbwAAABEAAAAVolbUzJjbWFwAAACAAAAAFAAAAFaNCnF72N2dCAAAAJQAAAACgAAAAoAAAAAZnBnbQAAAlwAAAWUAAALcIiQkFlnYXNwAAAH8AAAAAgAAAAIAAAAEGdseWYAAAf4AAAFIQAAB5Aqg1+6aGVhZAAADRwAAAAvAAAANgs/y+hoaGVhAAANTAAAAB8AAAAkD4gG32htdHgAAA1sAAAAOAAAAExf6AFkbG9jYQAADaQAAAAoAAAAKA0UDxRtYXhwAAANzAAAAB4AAAAgALUAX25hbWUAAA3sAAABUQAAAo4VwGZqcG9zdAAAD0AAAACYAAAAzEaEO/VwcmVwAAAP2AAAAGUAAAB73WsDhXdlYmYAABBAAAAABgAAAAaP4VXgAAAAAQAAAADMPaLPAAAAANAeRhwAAAAA0gZAYHjaY2BkYGDgA2IJBhBgYmAEQiEgZgHzGAAFEABFAAAAeNpjYGQNZpzAwMrAwirEOouBgVEeQjNfZ0hhEmBgYGJgZWbACgLSXFMYHFT/vOBnO/vvLMMOtrMMS4DCjCA5AKFmDF942mNgYGBmgGAZBkYGEAgB8hjBfBYGCyDNxcDBwASEDKp/XrC+4P//H6QIyGZ4wQ5i32KXYJFghuqFAkY2BrgAI1AnSDcKYGQY9gAAyR8MCgAAAAAAAAAAAAAAAHjarVZpcxNHEJ3VYcs2PoIPEjaBWcZyjHZWmMsIEMbsShbgHPKV7EKOXUt27otP/Ab9ml6RVJFv/LS8Hh3YYCdVVChK/ab37Uz3655ek9CSxF5Yj6TcfCmmtjZpZOdJSDdsWo7iQ9nZCylTTP4uiIJotdS+7TgkIhKBqnWFJYLY98jSJONDjzJatiW9alJu6Ul32RoP6q369tPQUY7dCSU1m6FD65EtqcKoEkUy7ZGSNi3D1V9JWuHnK8x81QwlgugkksabYQyP5GfjjFYZrcZ2HEWRTZYbRYpEMzyIIo+yWmKfXDFBQPmgGVJe+TSifIQfkRV7lNMKccl2mt/3JT/pHc6/JOJ6i7IlB/5AdmQHe6cr+SLS2grjpp1sR6GK8HR9J8Qjm5Pqn+xRXtNo4HZFpifNCJbKV5BY+Qll9g/JauF8ypc8GtWSg5wIWi9zYl/yDrQeR0yJaybIgu6OToig7pecodhj+rj4471dLBchBMg4lvWOSrgQRilhs5okbQQ5iJKyRZXUekdMnPI6LeItYb9O7ehLZ7RJqDsxnq2Hjq2cqOR4NKnTTKZO7aTm0ZQGUUo6Ezzm1wGUH9Ekr7axmsTKo2lsM2MkkVCghXNpKohlJ5Y0BdE8mtGbu2Gaa9eiRZo8UM89ek9vboWbOz2n7cA/a/xndSqmg70wnZ4OyEp8mna5SdG6fnqGfybxQ9YCKpEtNsOUxUO2fgfl5WNLjsJrA2z3nvMr6H32RMikgfgb8B4v1SkFTIWYVVAL3bTWtSzL1GpWi1Rk6rshTStf1mkCTTkOfWNfxjj+r5kZS0wJ3+/E6dkRl5659iXINIfcZl2P5nVqsV2AzmzP6TTL9n2d5th+oNM82/M6HWFr63SU7Yc6LbD9SKdjbC9oQZPuOwRyEYFcwAYSgbB1EAjbSwiErUIgbBcRCNsiAmG7hEDYfoxA2C4jELaXtayafippHDsTywBFiAOjOe7IZW4qV1PJpRKui0anNuQpcqukonhW/SsD/eKRN6yBtUC6RNb8ikmufFSV44+uaHnTxLkCjlV/e3NcnxMPZb9Y+FPwv9qaqqRXrHlkchV5I9CT40TXJhWPrunyuapH1/+Lig5rgX4DpRALRVmWDb6ZkPBRp9NQDVzlEDMbMw/X9bplzc/h/JsYIQvofvw3FBoL3INOWUlZ7WCv1dePZbm3B+WwJ1iSYr7M61vhi4zMSvtFZil7PvJ5wBUwKpVhqw1creDNexLzkOlN8kwQtxVlg6SNx5kgsYFjHjBvvpMgJExdtYHaKZywgbxgzCnY74RDVG+U5XB7oX0ejZR/a1fsyBkVTRD4bfZG2OuzUPJbrIGEJ7/U10BVIU3FuKmASyPlhmrwYVyt20YyTqCvqNgNy7KKDx9H3HdKjmUg+UgRq0dHP629Qp3Uuf3KKG7fO/0IgkFpYv72vpnioJR3tZJlVm0DU7calVPXmsPFqw7dzaPue8fZJ3LWNN10T9z0vqZVt4ODuVkQ7dsclKVMLqjrww4bqMvNpdDqZVyS3nYPMCwwoN+hFRv/V/dx+DxXqgqj40i9nagfo89iDPIPOH9H9QXo5zFMuYaU53uXE59u3MPZMl3FXayf4t/ArLXmZukacEPTDZiHrFodusoNfKcGOj3S3I70EPCx7grxAGATwGLwie5axvMpgPF8xhwf4HPmMGgyh8EWcxhsM2cNYIc5DHaZw2CPOQy+YM46wJfMYRAyh0HEHAZPmBMAPGUOg6+Yw+Br5jD4hjn3Ab5lDoOYOQwS5jDY13RrKHOLF3QXqG1QFejA9BMW97A41FQZsr/jhWF/bxCzfzCIqT9quj2k/sQLQ/3ZIKb+YhBTf9V0Z0j9jReG+rtBTP3DIKY+0y/GcpnBX0a+S4UDyi42n/P3xPsHwhpAtgABAAH//wAPeNp9VU1oG0cUnje7WlmKvJKsXa0dR9qMFK+symslki1VUezEgtiN6gajhkBFUcE9RSY1rZuWYoIIpqQmh+IkB9OmoYeS+lCCoTQ/t+TQlBB0aKGU4qan5pBLKD4EHOR13+zKpHFJdRjNmzfvm+/Nm/ctAbL9EwgpCHzcWpfWXCvET3SSJt6bA/He0C5RMFMjMJTLs0zYrSpSgsUMYXgop73EdqNtRqQJ3aRJtnFbT9L/sYoX9X5hcM9FfYD201OXIwM0Gb0cQS89tWRvWuIRhCBXqIlLwjqRifS9VwAzFXQoSW6QYgnAI0USGRC7LKIVwjgIKzzyIJ9qlISJg6EhxoqDIdoYeRgFDcKKFEfauFEZUXFIRltET0JND6MBKmKM9PcTG6NMFuiaS3uOEUKMHKYeVGweZYUigwNicixkEYU2dW4P7mmRiPmcB2IsIUYnYuziGMPBIYcGT8YwC5yIwjMxmyLR+109CDXKF0w7/gKh7jOuKaJifNBjcziMHDQPDnobBalc0K+w6l6Yok/w74qut5KsyjZ/0umKyb5kzLpO/2bsCnuLtZK6TgvMxj679dD1wJUi+xC7l2Pnh/IeMHYc4MHBz0/x4JAG4yyDKWuVIWwVZzDlHGitwpSu8xOsVfRW0f0orTt+mwH6MUav6dyv16IYw3EwZi8hFN9jTVqTiiROxknHD9lEr0DNlIqFz/LC+yEK2cy+fC6f07KZcAQoYbF9Cbc0CKHhLsIyopZvF4YXN3cYjHhMdEtuKQX8iVL7vVytNN9fBs+Ub7+3+4+AUrYGrXvXrPvzl/DylTL8BgevQZFbWFJmOSErj76wntoRf8k9MsfBkn01WVluKHO90O3zWE1roXHmsgamcL0DsnC28eElzYxguIlP8upVe2OP03lb63ADc9zuuVS8V/EJ7Z7jedk9FXd6LN+2E227CI7NEyqZa+YYHUmvpUfpS+a1h2aJjqYfmiN0jJ7ZduIS/QT3jPF5iZCO9r0v4CxIduPtD5BhMkomiHqk6+jYyKvZ/a8YLNKt+H0S7bAbMRPl7zaEjzgsA14/rmBLxQzgExm4y54MAvxrTxRkuu2JGfXlGXHu87nmR9/Mi3PfvcemP52ePl+Dewurn4mN2/PjJz+uipXGcSqXZ94Qj86VWO7YQTFTMX8sVcfEwrsF2pys15frdTp54gPX/NcI0joxXnvnQq3mYm83GrcajWfXiic5ypuwe7gsVqYrHSWx0FcQD1QOiAWrD2+gyvMXSNdWzf1EamL2cZLFmgwmoqpdE64WmTA2qlsGrnMJA0JKGKuSy4eGEgaWwS3hApYth+XjtqR2CbNd48fHFeF0MqWUEhuxvqOhlDAX6vb/ElCp6pX9z2S/N6TIf8qKPeSlcjyZjG/ceG0i3ocSOCHObj71Bmmnj8o+j8/naR3CwB7594CqBojTJ9+KSy6ujRHsk90hL6V2WbJtfTRQDIK2TNlS5yj0i3K5U/rW7TeNDkAJVOFXW8Y0LkPbeszPpEVHByn857ydB+xAdHRQqLV5t3VwB8kdpFDSX2Th5L7eQbBWCXIEcz+UjouYe0/7exUBu/cl1IAwfp9yqLN2z6htTQA8L2E4xDVcM9Lg6NndRaGPl6O1NjlDZ8vCjcm6GigH1LuLrfLiXUHupI8fKElGJ/hHpqDd39S8zHuuE4rCzfN3qMK3zrzeOjZZp7PH4DQvlHVp8c4d+Dmwiz6+j9JiRjZv6Xg9DzbDPt+5TkzlH7P8hqoAAAB42mNgZGBgAOI5Ufuj4vltvjLIczCAwCU2hwQE/T+Og4HtLJDLwcAEEgUACxEJCgB42mNgZGBgO/vvLMMODgYGhv//gCRQBAUIAwCOkwV5AHja42CAAKZVDAwsQJrtNpCeCMFMtxm8gJiBg4Ghm82doRYkx7iNgYGdASwmBFMHwkA+AwBxgwmlAAAAAAAAAAAACABWAHgAmgC8ANwBCgFQAcICDAKMAuQDHgNAA2IDyHjaY2BkYGAQZvBg4GAAASYwWQzEagyiICYAEIYBFQAAeNp1kU1OwlAUhc8TNP4kjAwDRx0YoxNEgsYw0mhwrgmOC1YoImKpJk6IC3ARrsA4dAHG+LMCXYRr8OvrowOCaW577rnn3nfuq6SCnpSTyS9I6hIpNlomS/EMmpHDOfgHh/Na1aPDsyrq1eE5ND8OL6qhX4eXtGYOHX5X0Yznf6hs7h3+1Lx5dvhLBfOS4u+cVsybDnSlge4UKVRbHcXytA67wbeisrZUBTVReChTVai+fPVgfN3Q0bGVIfkecU7Whw1Q9MAltXhfUo/Rx3QMOdFnTqBjog3TI4+mKqZx3kRfgyzpDu3JHp5LOJ/WeUT0rdq3Ds+y3Ya6RVGBjdkg2SKyrj3VJ/bxmJbUujAt+JK9tRi2pk2e//Y/hWlm1bHLur1TT/v4bNs737W1CtOqPDXwTvYvtnXBlICzBm7nwO5Xz6ae6BompBYlDv4AFmJmQgAAAHjabY1bDsIgFEQZrAV8G90GPyauwMR9NC0qKV4aHtblK/XXk8zM+RvG2Y8j+8/hGzAOjhkqzFFDQEJhgSVWWGODLXbYi0z2dL5eqqehLDs/ku5sqPNQRjpzS0VUsPfHZNz3ddtQa5xqQvBj1O24Ns7ZIdqoXyYkEQdLZIJqUjKUrKcq+pBUKV0OxGR5kM5Sr807fQBTyi/YeNpj8N7BcCIoYiMjY1/kBsadHAwcDMkFGxlYnTYyMGhBaA4UeicDAwMnMouZwWWjCmNHYMQGh46IjcwpLhvVQLxdHA0MjCwOHckhESAlkUCwkYFHawfj/9YNLL0bmRhcAAfTIrgAAAAAAVXgj+AAAA==) format('woff');
-  font-weight: normal;
-  font-style: normal;
-}
-
-[class^="icon-"]:before, [class*=" icon-"]:before {
-  font-family: "statuspage";
-  font-style: normal;
-  font-weight: normal;
-  speak: none;
-
-  display: inline-block;
-  text-decoration: inherit;
-  width: 1em;
-  margin-right: .2em;
-  text-align: center;
-  /* opacity: .8; */
-
-  /* For safety - reset parent styles, that can break glyph codes*/
-  font-variant: normal;
-  text-transform: none;
-
-  /* fix buttons height, for twitter bootstrap */
-  line-height: 1em;
-
-  /* Animation center compensation - margins should be symmetric */
-  /* remove if not needed */
-  margin-left: .2em;
-
-  /* you can be more comfortable with increased icons size */
-  /* font-size: 120%; */
-
-  /* Uncomment for 3D effect */
-  /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
-}
-
-.icon-menu:before { content: '\e800'; } /* 'î €' */
-.icon-down-dir:before { content: '\e801'; } /* '' */
-.icon-up-dir:before { content: '\e802'; } /* 'î ‚' */
-.icon-left-dir:before { content: '\e803'; } /* 'î ƒ' */
-.icon-right-dir:before { content: '\e804'; } /* 'î „' */
-.icon-ok:before { content: '\e805'; } /* 'î …' */
-.icon-cancel:before { content: '\e807'; } /* 'î ‡' */
-.icon-arrows-cw:before { content: '\e808'; } /* 'î ˆ' */
-.icon-ellipsis-vert:before { content: '\e809'; } /* 'î ‰' */
-.icon-spinner:before { content: '\e80a'; } /* 'î Š' */
-.icon-attention:before { content: '\e80b'; } /* 'î ‹' */
-.icon-sort:before { content: '\e80c'; } /* '' */
-.icon-sort-down:before { content: '\e80d'; } /* '' */
-.icon-sort-up:before { content: '\e80e'; } /* 'î Ž' */
-.icon-link-ext:before { content: '\e80f'; } /* '' */
diff --git a/package/gluon-status-page/src/css/main.css b/package/gluon-status-page/src/css/main.css
deleted file mode 100644
index 0604802e6..000000000
--- a/package/gluon-status-page/src/css/main.css
+++ /dev/null
@@ -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;
-  }
-}
diff --git a/package/gluon-status-page/src/css/menu.css b/package/gluon-status-page/src/css/menu.css
deleted file mode 100644
index b29e4a418..000000000
--- a/package/gluon-status-page/src/css/menu.css
+++ /dev/null
@@ -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);
-}
diff --git a/package/gluon-status-page/src/css/reset.css b/package/gluon-status-page/src/css/reset.css
deleted file mode 100644
index f6ca1484a..000000000
--- a/package/gluon-status-page/src/css/reset.css
+++ /dev/null
@@ -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;
-}
diff --git a/package/gluon-status-page/src/index.html.m4 b/package/gluon-status-page/src/index.html.m4
deleted file mode 100644
index 2726f2e34..000000000
--- a/package/gluon-status-page/src/index.html.m4
+++ /dev/null
@@ -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>
diff --git a/package/gluon-status-page/src/js/lib/gui.js b/package/gluon-status-page/src/js/lib/gui.js
deleted file mode 100644
index 3d7710050..000000000
--- a/package/gluon-status-page/src/js/lib/gui.js
+++ /dev/null
@@ -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
-  }
-})
diff --git a/package/gluon-status-page/src/js/lib/gui/menu.js b/package/gluon-status-page/src/js/lib/gui/menu.js
deleted file mode 100644
index 712f2d0f6..000000000
--- a/package/gluon-status-page/src/js/lib/gui/menu.js
+++ /dev/null
@@ -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)
-      }
-    }
-  }
-})
diff --git a/package/gluon-status-page/src/js/lib/gui/neighbours.js b/package/gluon-status-page/src/js/lib/gui/neighbours.js
deleted file mode 100644
index f6ce9e42b..000000000
--- a/package/gluon-status-page/src/js/lib/gui/neighbours.js
+++ /dev/null
@@ -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
-           }
-  }
-})
diff --git a/package/gluon-status-page/src/js/lib/gui/nodeinfo.js b/package/gluon-status-page/src/js/lib/gui/nodeinfo.js
deleted file mode 100644
index 4fc081238..000000000
--- a/package/gluon-status-page/src/js/lib/gui/nodeinfo.js
+++ /dev/null
@@ -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 () {}
-           }
-  }
-})
diff --git a/package/gluon-status-page/src/js/lib/gui/signal.js b/package/gluon-status-page/src/js/lib/gui/signal.js
deleted file mode 100644
index d91c63fb5..000000000
--- a/package/gluon-status-page/src/js/lib/gui/signal.js
+++ /dev/null
@@ -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
-  }
-})
diff --git a/package/gluon-status-page/src/js/lib/gui/signalgraph.js b/package/gluon-status-page/src/js/lib/gui/signalgraph.js
deleted file mode 100644
index 231ce6289..000000000
--- a/package/gluon-status-page/src/js/lib/gui/signalgraph.js
+++ /dev/null
@@ -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
-  }
-})
diff --git a/package/gluon-status-page/src/js/lib/gui/statistics.js b/package/gluon-status-page/src/js/lib/gui/statistics.js
deleted file mode 100644
index 2f3e6e27a..000000000
--- a/package/gluon-status-page/src/js/lib/gui/statistics.js
+++ /dev/null
@@ -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
-           }
-  }
-})
diff --git a/package/gluon-status-page/src/js/lib/helper.js b/package/gluon-status-page/src/js/lib/helper.js
deleted file mode 100644
index 75bace13a..000000000
--- a/package/gluon-status-page/src/js/lib/helper.js
+++ /dev/null
@@ -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
-         , _: _
-         }
-})
diff --git a/package/gluon-status-page/src/js/lib/neighbourstream.js b/package/gluon-status-page/src/js/lib/neighbourstream.js
deleted file mode 100644
index dbe42a344..000000000
--- a/package/gluon-status-page/src/js/lib/neighbourstream.js
+++ /dev/null
@@ -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)
-  }
-})
diff --git a/package/gluon-status-page/src/js/lib/streams.js b/package/gluon-status-page/src/js/lib/streams.js
deleted file mode 100644
index 68667cb20..000000000
--- a/package/gluon-status-page/src/js/lib/streams.js
+++ /dev/null
@@ -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
-         }
-})
diff --git a/package/gluon-status-page/src/js/main.js b/package/gluon-status-page/src/js/main.js
deleted file mode 100644
index eb6686987..000000000
--- a/package/gluon-status-page/src/js/main.js
+++ /dev/null
@@ -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)
-  })
-})
-- 
GitLab