gluon-status-page: new status page
This commit is contained in:
parent
faba9efb88
commit
5e5dc5ab18
@ -5,9 +5,32 @@ PKG_VERSION:=1
|
|||||||
PKG_RELEASE:=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
|
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
|
define Package/gluon-status-page
|
||||||
SECTION:=gluon
|
SECTION:=gluon
|
||||||
CATEGORY:=Gluon
|
CATEGORY:=Gluon
|
||||||
@ -22,16 +45,23 @@ endef
|
|||||||
|
|
||||||
define Build/Prepare
|
define Build/Prepare
|
||||||
mkdir -p $(PKG_BUILD_DIR)
|
mkdir -p $(PKG_BUILD_DIR)
|
||||||
|
$(CP) -t $(PKG_BUILD_DIR) $(DL_DIR)/r.js $(DL_DIR)/Bacon.js $(DL_DIR)/almond.js
|
||||||
endef
|
endef
|
||||||
|
|
||||||
define Build/Configure
|
define Build/Configure
|
||||||
|
$(CP) ./src/* $(PKG_BUILD_DIR)/
|
||||||
endef
|
endef
|
||||||
|
|
||||||
define Build/Compile
|
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
|
endef
|
||||||
|
|
||||||
define Package/gluon-status-page/install
|
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
|
endef
|
||||||
|
|
||||||
$(eval $(call BuildPackage,gluon-status-page))
|
$(eval $(call BuildPackage,gluon-status-page))
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta http-equiv="refresh" content="0; URL=/cgi-bin/status">
|
|
||||||
<meta http-equiv="cache-control" content="no-cache">
|
|
||||||
<meta http-equiv="expires" content="0">
|
|
||||||
<meta http-equiv="expires" content="Tue, 01 Jan 1980 1:00:00 GMT">
|
|
||||||
<meta http-equiv="pragma" content="no-cache">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<a href="/cgi-bin/status">Redirecting...</a>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
100
package/gluon-status-page/iconfont-config.json
Normal file
100
package/gluon-status-page/iconfont-config.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
10
package/gluon-status-page/src/build.js
Normal file
10
package/gluon-status-page/src/build.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
({
|
||||||
|
paths: {
|
||||||
|
"bacon": "../Bacon"
|
||||||
|
},
|
||||||
|
baseUrl: "js/",
|
||||||
|
name: "../almond",
|
||||||
|
include: "main",
|
||||||
|
optimize: "uglify2",
|
||||||
|
out: "app.js",
|
||||||
|
})
|
85
package/gluon-status-page/src/css/animation.css
Normal file
85
package/gluon-status-page/src/css/animation.css
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
53
package/gluon-status-page/src/css/font.css
Normal file
53
package/gluon-status-page/src/css/font.css
Normal file
File diff suppressed because one or more lines are too long
171
package/gluon-status-page/src/css/main.css
Normal file
171
package/gluon-status-page/src/css/main.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
60
package/gluon-status-page/src/css/menu.css
Normal file
60
package/gluon-status-page/src/css/menu.css
Normal file
@ -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);
|
||||||
|
}
|
86
package/gluon-status-page/src/css/reset.css
Normal file
86
package/gluon-status-page/src/css/reset.css
Normal file
@ -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;
|
||||||
|
}
|
17
package/gluon-status-page/src/index.html.m4
Normal file
17
package/gluon-status-page/src/index.html.m4
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||||
|
<style>
|
||||||
|
undivert(style.css)
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
var bootstrapUrl = "/cgi-bin/nodeinfo";
|
||||||
|
|
||||||
|
undivert(app.js)
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
</body>
|
||||||
|
</html>
|
157
package/gluon-status-page/src/js/lib/gui.js
Normal file
157
package/gluon-status-page/src/js/lib/gui.js
Normal file
@ -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
|
||||||
|
}
|
||||||
|
})
|
39
package/gluon-status-page/src/js/lib/gui/menu.js
Normal file
39
package/gluon-status-page/src/js/lib/gui/menu.js
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
275
package/gluon-status-page/src/js/lib/gui/neighbours.js
Normal file
275
package/gluon-status-page/src/js/lib/gui/neighbours.js
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
54
package/gluon-status-page/src/js/lib/gui/nodeinfo.js
Normal file
54
package/gluon-status-page/src/js/lib/gui/nodeinfo.js
Normal file
@ -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 () {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
48
package/gluon-status-page/src/js/lib/gui/signal.js
Normal file
48
package/gluon-status-page/src/js/lib/gui/signal.js
Normal file
@ -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
|
||||||
|
}
|
||||||
|
})
|
137
package/gluon-status-page/src/js/lib/gui/signalgraph.js
Normal file
137
package/gluon-status-page/src/js/lib/gui/signalgraph.js
Normal file
@ -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
|
||||||
|
}
|
||||||
|
})
|
272
package/gluon-status-page/src/js/lib/gui/statistics.js
Normal file
272
package/gluon-status-page/src/js/lib/gui/statistics.js
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
71
package/gluon-status-page/src/js/lib/helper.js
Normal file
71
package/gluon-status-page/src/js/lib/helper.js
Normal file
@ -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
|
||||||
|
}
|
||||||
|
})
|
132
package/gluon-status-page/src/js/lib/neighbourstream.js
Normal file
132
package/gluon-status-page/src/js/lib/neighbourstream.js
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
})
|
66
package/gluon-status-page/src/js/lib/streams.js
Normal file
66
package/gluon-status-page/src/js/lib/streams.js
Normal file
@ -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
|
||||||
|
}
|
||||||
|
})
|
108
package/gluon-status-page/src/js/main.js
Normal file
108
package/gluon-status-page/src/js/main.js
Normal file
@ -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)
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in New Issue
Block a user