diff --git a/package/gluon-status-page/Makefile b/package/gluon-status-page/Makefile
index 7b496ddd..c283f928 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 00000000..11969ddc
--- /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")
+-%>
+
+
+
+
+
+
+ <%:Error%>
+
+
+
+
+
+
+
+ <% renderer.render(content, scope, pkg) %>
+
+
+
+
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 00000000..2d023af7
--- /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(' ', pcdata(key), pcdata(format or 'id'))
+ end
+
+ local function statisticsTraffic(key)
+ return string.format('%s %s %s',
+ statistics(key .. '/packets', 'packetsDiff'),
+ statistics(key .. '/bytes', 'bytesDiff'),
+ statistics(key .. '/bytes', 'bytes')
+ )
+ end
+
+ http:prepare_content("application/xhtml+xml")
+-%>
+
+
+
+
+
+
+ <%| nodeinfo.hostname %> - <%:Status%>
+
+
+
+ >
+
+ <%| nodeinfo.hostname %>
+
+
+
+
<%:Overview%>
+
+ <%:Node name%> <%| nodeinfo.hostname %>
+ <%:Model%> <%| nodeinfo.hardware.model %>
+ <%:Primary MAC address%> <%| nodeinfo.network.mac %>
+ <%:IP address%> <%= pcdata(table.concat(sorted(nodeinfo.network.addresses), '\n')):gsub('\n', ' ') %>
+ <%:Firmware%> <%| nodeinfo.software.firmware.release %>
+ <% if nodeinfo.software.fastd then -%>
+ <%:Mesh VPN%> <%| enabled(nodeinfo.software.fastd.enabled) %>
+ <%- end %>
+ <% if nodeinfo.software.autoupdater then -%>
+ <%:Automatic updates%> <%| enabled(nodeinfo.software.autoupdater.enabled) %><%|
+ nodeinfo.software.autoupdater.enabled and
+ string.format(' (%s)', nodeinfo.software.autoupdater.branch)
+ %>
+ <%- end %>
+
+
+
+
<%:Monitoring%>
+
+ <%:Uptime%> <%= statistics('uptime', 'time') %>
+ <%:Load average%> <%= statistics('loadavg', 'decimal') %>
+ <%:RAM%> <%= statistics('memory', 'memory') %>
+ <%:Filesystem%> <%= statistics('rootfs_usage', 'percent') %>
+ <%:Gateway%> <%= statistics('gateway') %>
+ <%:Clients%> <%= statistics('clients/total') %>
+
+
+
<%:Traffic%>
+
+ <%:Transmitted%> <%= statisticsTraffic('traffic/tx') %>
+ <%:Received%> <%= statisticsTraffic('traffic/rx') %>
+ <%:Forwarded%> <%= statisticsTraffic('traffic/forward') %>
+
+
+
+
+
+
<%:Neighbors%>
+
+ <%
+ for _, iface in ipairs(interfaces) do
+ local wireless = is_wireless(iface)
+ local address = fs.readfile('/sys/class/net/' .. iface .. '/address')
+ if address then
+ %>
+
<%| iface %>
+
>
+
+
+ <%:Node%>
+ TQ
+ <% if wireless then %>
+ dBm
+ <%:Distance%>
+ <%:Last seen%>
+ <% end %>
+
+
+
+ <%
+ end
+ end
+ %>
+
+
+
+
+
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 00000000..d5da1c84
--- /dev/null
+++ b/package/gluon-status-page/files/lib/gluon/status-page/www/index.html
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
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 00000000..a6a974e2
--- /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 00000000..55ee27a6
--- /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&&r1&&(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;u40&&(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;ne[0]?-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 00000000..b6ff1a78
--- /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: \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 00000000..b4c3882e
--- /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 00000000..3f5101af
--- /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 af8718cc..00000000
--- 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 00000000..7a890bdc
--- /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 00000000..18ea1864
--- /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 00000000..7e5079ac
--- /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 00000000..cfaa561f
--- /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 a1b1d703..00000000
--- 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 e6edbf3c..00000000
--- 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 c26ee355..00000000
--- 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,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) 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 0604802e..00000000
--- 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 b29e4a41..00000000
--- 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 f6ca1484..00000000
--- 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 2726f2e3..00000000
--- a/package/gluon-status-page/src/index.html.m4
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
-
-
- Bitte Javascript aktivieren.
-
-
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 3d771005..00000000
--- 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 712f2d0f..00000000
--- 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 f6ce9e42..00000000
--- 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 4fc08123..00000000
--- 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 d91c63fb..00000000
--- 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 231ce628..00000000
--- 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 2f3e6e27..00000000
--- 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 75bace13..00000000
--- 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 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)
- })
-})