From 5e5dc5ab1852d3753649236e5bdd7774b0f45556 Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Mon, 18 Aug 2014 14:58:16 +0200 Subject: [PATCH] gluon-status-page: new status page --- package/gluon-status-page/Makefile | 34 ++- .../lib/gluon/status-page/www/index.html | 12 - .../gluon-status-page/iconfont-config.json | 100 +++++++ package/gluon-status-page/src/build.js | 10 + .../gluon-status-page/src/css/animation.css | 85 ++++++ 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 | 60 ++++ package/gluon-status-page/src/css/reset.css | 86 ++++++ package/gluon-status-page/src/index.html.m4 | 17 ++ 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 | 275 ++++++++++++++++++ .../src/js/lib/gui/nodeinfo.js | 54 ++++ .../src/js/lib/gui/signal.js | 48 +++ .../src/js/lib/gui/signalgraph.js | 137 +++++++++ .../src/js/lib/gui/statistics.js | 272 +++++++++++++++++ .../gluon-status-page/src/js/lib/helper.js | 71 +++++ .../src/js/lib/neighbourstream.js | 132 +++++++++ .../gluon-status-page/src/js/lib/streams.js | 66 +++++ package/gluon-status-page/src/js/main.js | 108 +++++++ 21 files changed, 1973 insertions(+), 14 deletions(-) delete mode 100644 package/gluon-status-page/files/lib/gluon/status-page/www/index.html create mode 100644 package/gluon-status-page/iconfont-config.json create mode 100644 package/gluon-status-page/src/build.js create mode 100644 package/gluon-status-page/src/css/animation.css create mode 100644 package/gluon-status-page/src/css/font.css create mode 100644 package/gluon-status-page/src/css/main.css create mode 100644 package/gluon-status-page/src/css/menu.css create mode 100644 package/gluon-status-page/src/css/reset.css create mode 100644 package/gluon-status-page/src/index.html.m4 create mode 100644 package/gluon-status-page/src/js/lib/gui.js create mode 100644 package/gluon-status-page/src/js/lib/gui/menu.js create mode 100644 package/gluon-status-page/src/js/lib/gui/neighbours.js create mode 100644 package/gluon-status-page/src/js/lib/gui/nodeinfo.js create mode 100644 package/gluon-status-page/src/js/lib/gui/signal.js create mode 100644 package/gluon-status-page/src/js/lib/gui/signalgraph.js create mode 100644 package/gluon-status-page/src/js/lib/gui/statistics.js create mode 100644 package/gluon-status-page/src/js/lib/helper.js create mode 100644 package/gluon-status-page/src/js/lib/neighbourstream.js create mode 100644 package/gluon-status-page/src/js/lib/streams.js create 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 2543ea2a..60a267cf 100644 --- a/package/gluon-status-page/Makefile +++ b/package/gluon-status-page/Makefile @@ -4,10 +4,33 @@ PKG_NAME:=gluon-status-page PKG_VERSION:=1 PKG_RELEASE:=1 -PKG_BUILD_DIR := $(BUILD_DIR)/$(PKG_NAME) +PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME) +PKG_BUILD_DEPENDS:=node/host include $(INCLUDE_DIR)/package.mk +define Download/rjs + FILE:=r.js + URL:=http://requirejs.org/docs/release/2.1.10 + MD5SUM:=270154b3f5d417c3a42f1e58d03e6607 +endef + +define Download/Bacon + FILE:=Bacon.js + URL:=http://cdnjs.cloudflare.com/ajax/libs/bacon.js/0.7.71 + MD5SUM:=4600a60e1d7ffdb2259dfcce97c860ed +endef + +define Download/almond + FILE:=almond.js + URL:=https://raw.githubusercontent.com/jrburke/almond/0.3.1 + MD5SUM:=aa66c0c0cb55a4627bb706df73f3aff5 +endef + +$(eval $(call Download,rjs)) +$(eval $(call Download,Bacon)) +$(eval $(call Download,almond)) + define Package/gluon-status-page SECTION:=gluon CATEGORY:=Gluon @@ -22,16 +45,23 @@ endef define Build/Prepare mkdir -p $(PKG_BUILD_DIR) + $(CP) -t $(PKG_BUILD_DIR) $(DL_DIR)/r.js $(DL_DIR)/Bacon.js $(DL_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 endef define Package/gluon-status-page/install - $(CP) ./files/* $(1)/ + $(INSTALL_DIR) $(1)/lib/gluon/status-page/www/ + $(INSTALL_DATA) $(PKG_BUILD_DIR)/index.html $(1)/lib/gluon/status-page/www/ endef $(eval $(call BuildPackage,gluon-status-page)) 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 deleted file mode 100644 index 75700015..00000000 --- a/package/gluon-status-page/files/lib/gluon/status-page/www/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - Redirecting... - - diff --git a/package/gluon-status-page/iconfont-config.json b/package/gluon-status-page/iconfont-config.json new file mode 100644 index 00000000..af8718cc --- /dev/null +++ b/package/gluon-status-page/iconfont-config.json @@ -0,0 +1,100 @@ +{ + "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/src/build.js b/package/gluon-status-page/src/build.js new file mode 100644 index 00000000..a1b1d703 --- /dev/null +++ b/package/gluon-status-page/src/build.js @@ -0,0 +1,10 @@ +({ + 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 new file mode 100644 index 00000000..ac5a9562 --- /dev/null +++ b/package/gluon-status-page/src/css/animation.css @@ -0,0 +1,85 @@ +/* + Animation example, for spinners +*/ +.animate-spin { + -moz-animation: spin 2s infinite linear; + -o-animation: spin 2s infinite linear; + -webkit-animation: spin 2s infinite linear; + animation: spin 2s infinite linear; + display: inline-block; +} +@-moz-keyframes spin { + 0% { + -moz-transform: rotate(0deg); + -o-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + + 100% { + -moz-transform: rotate(359deg); + -o-transform: rotate(359deg); + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@-webkit-keyframes spin { + 0% { + -moz-transform: rotate(0deg); + -o-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + + 100% { + -moz-transform: rotate(359deg); + -o-transform: rotate(359deg); + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@-o-keyframes spin { + 0% { + -moz-transform: rotate(0deg); + -o-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + + 100% { + -moz-transform: rotate(359deg); + -o-transform: rotate(359deg); + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@-ms-keyframes spin { + 0% { + -moz-transform: rotate(0deg); + -o-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + + 100% { + -moz-transform: rotate(359deg); + -o-transform: rotate(359deg); + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@keyframes spin { + 0% { + -moz-transform: rotate(0deg); + -o-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + + 100% { + -moz-transform: rotate(359deg); + -o-transform: rotate(359deg); + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} diff --git a/package/gluon-status-page/src/css/font.css b/package/gluon-status-page/src/css/font.css new file mode 100644 index 00000000..c26ee355 --- /dev/null +++ b/package/gluon-status-page/src/css/font.css @@ -0,0 +1,53 @@ +@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 new file mode 100644 index 00000000..0604802e --- /dev/null +++ b/package/gluon-status-page/src/css/main.css @@ -0,0 +1,171 @@ +@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 new file mode 100644 index 00000000..c865c648 --- /dev/null +++ b/package/gluon-status-page/src/css/menu.css @@ -0,0 +1,60 @@ +.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; + -webkit-animation: new-menu-animation .08s ease-out forwards; + -moz-animation: new-menu-animation .08s ease-out forwards; +} + +@-webkit-keyframes new-menu-animation { + from { + transform: scaleY(0); + } + to { + transform: scaleY(1); + } +} + +@-moz-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 new file mode 100644 index 00000000..f6ca1484 --- /dev/null +++ b/package/gluon-status-page/src/css/reset.css @@ -0,0 +1,86 @@ +/* +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 new file mode 100644 index 00000000..489c1cf0 --- /dev/null +++ b/package/gluon-status-page/src/index.html.m4 @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/package/gluon-status-page/src/js/lib/gui.js b/package/gluon-status-page/src/js/lib/gui.js new file mode 100644 index 00000000..3d771005 --- /dev/null +++ b/package/gluon-status-page/src/js/lib/gui.js @@ -0,0 +1,157 @@ +"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 new file mode 100644 index 00000000..712f2d0f --- /dev/null +++ b/package/gluon-status-page/src/js/lib/gui/menu.js @@ -0,0 +1,39 @@ +"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 new file mode 100644 index 00000000..3c2aaca7 --- /dev/null +++ b/package/gluon-status-page/src/js/lib/gui/neighbours.js @@ -0,0 +1,275 @@ +"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 = document.createElement("tr") + parent.appendChild(el) + + var tdHostname = document.createElement("td") + var tdTQ = document.createElement("td") + var tdSignal = document.createElement("td") + var tdDistance = document.createElement("td") + var tdInactive = document.createElement("td") + + el.appendChild(tdHostname) + el.appendChild(tdTQ) + el.appendChild(tdSignal) + el.appendChild(tdDistance) + el.appendChild(tdInactive) + + 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.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 = document.createElement("tr") + table.appendChild(tr) + table.classList.add("datatable") + + var th = document.createElement("th") + th.textContent = "Knoten" + 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 = "Entfernung" + tr.appendChild(th) + + th = document.createElement("th") + th.textContent = "Inaktiv" + 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) + } + + for (id in notUpdated) { + 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) + } + + 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("." + k), mgmtBus) + } + } + + function destroy() { + stopStream() + + while (div.firstChild) { + div.firstChild.destroy() + div.removeChild(div.firstChild) + } + } + + return { title: document.createTextNode("Nachbarknoten") + , 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 new file mode 100644 index 00000000..75910aae --- /dev/null +++ b/package/gluon-status-page/src/js/lib/gui/nodeinfo.js @@ -0,0 +1,54 @@ +"use strict" +define(["lib/helper"], function (Helper) { + return function (nodeInfo) { + var el = document.createElement("div") + + update(nodeInfo) + + function dlEntry(dl, dict, key, prettyName) { + var v = Helper.dictGet(dict, key.split(".")) + + if (v === null) + return + + 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 update(nodeInfo) { + var list = document.createElement("dl") + + dlEntry(list, nodeInfo, "hostname", "Knotenname") + dlEntry(list, nodeInfo, "owner.contact", "Kontakt") + dlEntry(list, nodeInfo, "hardware.model", "Modell") + dlEntry(list, nodeInfo, "network.mac", "Primäre MAC") + dlEntry(list, nodeInfo, "network.addresses", "IP-Adresse") + dlEntry(list, nodeInfo, "software.firmware.release", "Firmware") + dlEntry(list, nodeInfo, "software.fastd.enabled", "Mesh-VPN") + dlEntry(list, nodeInfo, "software.autoupdater.enabled", "Automatische Updates") + dlEntry(list, nodeInfo, "software.autoupdater.branch", "Branch") + + el.appendChild(list) + } + + return { title: document.createTextNode("Übersicht") + , 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 new file mode 100644 index 00000000..d91c63fb --- /dev/null +++ b/package/gluon-status-page/src/js/lib/gui/signal.js @@ -0,0 +1,48 @@ +"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 new file mode 100644 index 00000000..83732158 --- /dev/null +++ b/package/gluon-status-page/src/js/lib/gui/signalgraph.js @@ -0,0 +1,137 @@ +"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) + 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 new file mode 100644 index 00000000..33a845d8 --- /dev/null +++ b/package/gluon-status-page/src/js/lib/gui/statistics.js @@ -0,0 +1,272 @@ +"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) { + var tr = document.createElement("tr") + var th = document.createElement("th") + var td = streamElement("td", stream) + th.textContent = label + tr.appendChild(th) + tr.appendChild(td) + table.appendChild(tr) + + tr.destroy = function () { + td.destroy() + table.removeChild(tr) + } + + return tr + } + + function mkTrafficRow(table, children, label, stream, selector) { + var tr = document.createElement("tr") + var th = document.createElement("th") + var td = document.createElement("td") + th.textContent = label + + 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) + + tr.appendChild(th) + tr.appendChild(td) + table.appendChild(tr) + + 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)) + }) + } + }) + + 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 "nicht verbunden" + else + return "verbunden (" + prettyUptime(d.established) + ")" + } + + function prettyPackets(d) { + var v = new Intl.NumberFormat("de-DE", {maximumFractionDigits: 0}).format(d) + return v + " Pakete/s" + } + + function prettyPrefix(prefixes, step, d) { + var prefix = 0 + + while (d > step && prefix < 4) { + d /= step + prefix++ + } + + d = new Intl.NumberFormat("de-DE", {maximumSignificantDigits: 3}).format(d) + return d + " " + prefixes[prefix] + } + + function prettyBits(d) { + return prettyPrefix([ "bps", "kbps", "Mbps", "Gbps" ], 1024, d * 8) + } + + function prettyBytes(d) { + return prettyPrefix([ "B", "kB", "MB", "GB" ], 1024, d) + } + + 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 Tag, " + else if (days > 1) + out += days + " Tage, " + + out += hours + ":" + + if (minutes < 10) + out += "0" + + out += minutes + + return out + } + + function prettyNVRAM(usage) { + return new Intl.NumberFormat("de-DE", {maximumSignificantDigits: 3}).format(usage * 100) + "% belegt" + } + + function prettyLoad(load) { + return new Intl.NumberFormat("de-DE", {maximumSignificantDigits: 3}).format(load) + } + + 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, "Laufzeit", stream.map(".uptime").map(prettyUptime))) + children.push(mkRow(table, "Systemlast", 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, "Gateway", stream.map(".gateway"))) + children.push(mkRow(table, "Clients", stream.map(".clients.total"))) + + el.appendChild(table) + + var h = document.createElement("h3") + h.textContent = "Traffic" + el.appendChild(h) + + table = document.createElement("table") + + mkTrafficRow(table, children, "Gesendet", stream, ".traffic.rx") + mkTrafficRow(table, children, "Empfangen", stream, ".traffic.tx") + mkTrafficRow(table, children, "Weitergeleitet", 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("Statistik") + , 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 new file mode 100644 index 00000000..6b39bc0a --- /dev/null +++ b/package/gluon-status-page/src/js/lib/helper.js @@ -0,0 +1,71 @@ +"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 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 + } + + return { buildUrl: buildUrl + , request: request + , getJSON: getJSON + , dictGet: dictGet + , haversine: haversine + } +}) diff --git a/package/gluon-status-page/src/js/lib/neighbourstream.js b/package/gluon-status-page/src/js/lib/neighbourstream.js new file mode 100644 index 00000000..dbe42a34 --- /dev/null +++ b/package/gluon-status-page/src/js/lib/neighbourstream.js @@ -0,0 +1,132 @@ +"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 new file mode 100644 index 00000000..50286b74 --- /dev/null +++ b/package/gluon-status-page/src/js/lib/streams.js @@ -0,0 +1,66 @@ +"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) + } + + 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 new file mode 100644 index 00000000..3bdbe2a0 --- /dev/null +++ b/package/gluon-status-page/src/js/main.js @@ -0,0 +1,108 @@ +"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 + } + + if (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]) + + 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) + }) +})