Compare commits

..

66 commits

Author SHA1 Message Date
Alexander d70d3d7c5f Add Node.js LTS version in travis.yml (#105) 2019-02-01 06:52:56 +01:00
Milan Pässler fe17e32886 fix: roboto and ionicons 2019-01-26 04:55:33 +01:00
Milan Pässler 420be21fa5 add version number 2019-01-24 00:26:00 +01:00
Milan Pässler a971dcfeed update installation instructions 2019-01-20 20:21:17 +01:00
Milan Pässler 0a05523dd1 remove bower and jshashes 2019-01-20 18:15:14 +01:00
Milan Pässler f416c33498 fix(travis): update nodejs, build process 2019-01-20 15:18:59 +01:00
Milan Pässler 93e0a9c758 update deps, fix build with node v10 2019-01-20 15:08:12 +01:00
anoy d231cbd5bc
Merge pull request #102 from jjx-/master
Updated DOP-NRW WMS URL
2018-08-25 18:24:55 +02:00
JJX 11368e60de
Updated DOP-NRW WMS URL
Der alte WMS Dienst für Luftbilder des Landes wird in Kürze abgeschaltet, dies ist der Neue.
2018-08-24 11:40:27 +02:00
PetaByteBoy // Milan Pässler 587740af80 change map layers in default config 2017-04-12 02:22:47 +02:00
Simon Wüllhorst 1235d8cb46 lib/map: support wms layers. (#89) 2017-04-12 01:56:05 +02:00
Andreas Ziegler 100268f3b7 change links to new upstream project name in readme & about box (#91) 2017-04-02 16:13:05 +02:00
Milan Pässler 35589eabef fix html indention 2017-03-16 23:08:49 +01:00
H4ndl3 8f7c63bbce add loading animation
Create _loader.scss

Update main.scss

Update _base.scss

Update gui.js

Update index.html

removed trailing spaces

removed trailing white spaces for real
2017-03-16 19:54:57 +01:00
Milan Paessler b2c2627d73 infobox: fix error when gateway could not be resolved 2017-02-14 21:33:36 +01:00
kb-light a7756c44e8 infobox/link: linkinfos {SOURCE_NAME} and {TARGET_NAME} support (#82) 2017-02-11 01:21:20 +01:00
Daniel Krah 5f98da2717 Update README.md (#86)
change installation of sass on mac.
Version via gem is outdated
2017-02-11 01:20:04 +01:00
PetaByteBoy // Milan Pässler 1a6a4329b5 forcegraph: only draw clients when zoomed in 2017-02-08 09:35:26 +01:00
vsandre ccc5f0f526 nodelist: Add node id to the href of node name in the table (#84)
So it is possible to open multiple node pages in a new window/tab
2017-02-03 23:02:20 +01:00
Marvin W 5cb88e8d06 Use server side resolving only, add proper support for nexthop resolving (#81)
* Revert "Use resolved data if gateway was already resolved (#78)"

This reverts commit 44bb8e9d3d.

* Revert "proportions: lookup gateway name"

This reverts commit 94662cb3dc.

* Revert "infobox/node: change mac used to lookup nodes"

This reverts commit 2ca2604403.

* Revert "infobox/node: resolve nexthop and gateway from data"

This reverts commit 9e7049c9e3.

* Resolve nexthop full chain if possible

requires hopglass/hopglass-server#79

* Correctly handle sidecase when nodes on route report inconsistent gateway
2017-01-07 18:43:04 +01:00
Marvin W 44bb8e9d3d Use resolved data if gateway was already resolved (#78)
This is now very dirty, I think the old approach was better (although it requires a server side change)
2016-12-10 11:45:21 +01:00
Jonas Platte 7e6d054e98 scss: Fix a few CSS problems (#73)
* Set text color on body

* Fix not being able to scroll more then sidebar height in Firefox

* Fix button color being set by browser stylesheet instead of site stylesheet
2016-11-26 00:10:27 +01:00
Milan Pssler 94662cb3dc proportions: lookup gateway name 2016-11-19 11:01:25 +01:00
Milan Pssler 2ca2604403 infobox/node: change mac used to lookup nodes 2016-11-19 10:57:24 +01:00
Milan Paessler 0171ebe7e1 infobox/node: non-clickable mac adresses should be black 2016-11-19 02:27:21 +01:00
Milan Paessler 9e7049c9e3 infobox/node: resolve nexthop and gateway from data
not perfect yet: the mac address doesn't always match
2016-11-19 02:17:08 +01:00
Milan Paessler 7d145141c1 infobox/node: handle unknown model when showing router img 2016-11-19 02:05:28 +01:00
Milan Pssler 5a5ce1d346 forcegraph: decrease line radius to increase click accuracy 2016-11-17 23:06:30 +01:00
Milan Pssler 224240c1c4 forcegraph: bidirectional links 2016-11-17 19:47:56 +01:00
Milan Pssler 7eb0675be0 fix meshclients for orange nodes 2016-11-06 17:13:22 +01:00
Milan Pssler 1641bc2437 router pics hopglass changes
- don't use display block on the table, instead make the close button
  position absolute
- show the old style heading when no source is defined
- clean up the indention and some small things
2016-11-06 00:11:49 +01:00
Moorviper 84aee48229 Router pics (#37)
* add a padding to sidebar for better use on mobile devices

* allow other things as text in attributeEntry

Now it is possible to use pictures and other stuff in this function

* Show Router-Pic with double fallback

3 Möglichkeiten
1. in config wird externe Quelle angegeben
2. wenn nicht wird in ./nodes/ geseucht
3. ansonsten Knotenname + Knotenname in erster Zeile

extern via:
cdn

"hwImg": [
  { "thumbnail": "https://cdn.rawgit.com/Moorviper/meshviewer_hwpics/master/nodes/{MODELHASH}.svg",
    "caption": "Knoten {MODELHASH}"
  }
 ]

aktueller nicht cdn:
"hwImg": [
  { "thumbnail": "https://rawgit.com/Moorviper/meshviewer_hwpics/master/nodes/{MODELHASH}.svg",
    "caption": "Knoten {MODELHASH}"
  }
 ]
2016-11-05 23:03:46 +01:00
Milan Pssler e9711c6efc new example config 2016-11-05 23:01:25 +01:00
Milan Pssler 5599be5cd6 re-add uplink detection, this time generic
fixes l2tp uplink detection
2016-11-05 21:54:47 +01:00
Daniel Krah 808b8c1986 Count Clients in a Mesh (#61)
This functions count all clients in a meshcloud.

It count over wireless and cable links not over VPN links via l2tp or fastd.
2016-11-05 21:24:38 +01:00
Daniel Krah 00a8e5117d fix es6-shim/es6-shim.map (#62) 2016-09-03 01:31:26 +02:00
Marvin W cb065d8d07 Fully resolve gateway names in hopglass (#57)
Depends on hopglass/hopglass-server#56
2016-08-05 19:47:25 +02:00
Marvin W bf2e858c24 Make sure hopglass does not break when server reports nodes without hostname. (#58) 2016-08-01 23:25:29 +02:00
Milan Paessler 46de672dc9 main: support for ffmap-backend vpn link notation 2016-07-23 16:33:49 +00:00
Daniel Krah 07d5e3f636 check IPv4/IPv6 (#56) 2016-07-23 14:52:49 +00:00
codedust cfd778dadb Fix number of decimal places being displayed in the CPU load bar (#54) 2016-07-23 14:49:35 +00:00
Milan Paessler 8f7b1e15ce Revert "Router pics (#37)"
This reverts commit 8435885e5c.
2016-07-04 12:45:19 +02:00
Marvin W 760cca6806 Properly handle link types (#47)
* Properly handle link types

* fixup! Properly handle link types
2016-07-04 11:29:16 +02:00
eberhab 2844a203d5 iframe width relates to sidebar width (#44) 2016-07-04 11:00:43 +02:00
Marvin W 1b332508a0 Shorten device name in sidebar stats (#46)
This way, version codes on TP-Link devices and processor names are visible
2016-07-04 10:58:26 +02:00
Moorviper 8435885e5c Router pics (#37)
* add a padding to sidebar for better use on mobile devices

* allow other things as text in attributeEntry

Now it is possible to use pictures and other stuff in this function

* Show Router-Pic with double fallback

3 Möglichkeiten
1. in config wird externe Quelle angegeben
2. wenn nicht wird in ./nodes/ geseucht
3. ansonsten Knotenname + Knotenname in erster Zeile

extern via:
cdn

"hwImg": [
  { "thumbnail": "https://cdn.rawgit.com/Moorviper/meshviewer_hwpics/master/nodes/{MODELHASH}.svg",
    "caption": "Knoten {MODELHASH}"
  }
 ]

aktueller nicht cdn:
"hwImg": [
  { "thumbnail": "https://rawgit.com/Moorviper/meshviewer_hwpics/master/nodes/{MODELHASH}.svg",
    "caption": "Knoten {MODELHASH}"
  }
 ]
2016-07-04 10:57:04 +02:00
eberhab f80ea1ca8f infobox: added wifi channel, airtime and batctl nexthop (#38)
* infobox: added wifi channel, airtime and batctl nexthop

* simplify

* kill all semicolons

* update data locations

I'd suggest that channel information go under nodeinfo. Airtime and nexthop are changing variables which should go under statistics.

* filter out nodes with faulty nodeinfo

* additional nexthop key

The nexthop to the batman gateway has now been made available in gluon master where it will be called "gateway_nexthop" the "nexthop" key is also currently in use and should eventually be removed

* sidebar/node: cosmetic changes to gateway/nexthop

* remove data validity check

* move wifi helper function to sidebar/node

* cosmetics
2016-06-08 23:23:56 +02:00
Johannes Rudolph 2bbdd1e077 main: filter duplicate nodes from different data sources
* add jquery

* filter duplicate nodes by online state

* remove old stuff

* remove debug output

* remove curly braces
2016-06-05 22:55:26 +02:00
Moritz 2da97bca16 meshstats: show offline nodes (#39) 2016-06-03 00:43:08 +02:00
Milan Paessler 4c6ba69dd3 meshstats: don't count undefined as gateway 2016-06-02 23:17:06 +02:00
Milan Pässler 93355f28ba meshstats: dynamic gateway count 2016-06-02 22:44:42 +02:00
Xaver Maierhofer 1bcd2b797d remove obsolete Gemfile (#28) 2016-05-22 18:18:07 +02:00
Xaver Maierhofer 3971c36250 add .editorconfig (#25) 2016-05-22 12:30:44 +02:00
Marvin W 9719385076 map: dynamically select maxLayerZoom (#27)
Fixes clients/names being hidden accidentally (in certain conditions). Also added auto-zoom-out to ensure zoom is in bounds
2016-05-22 12:30:31 +02:00
Marvin W 20f0f08de3 Replace MapQuest with OpenStreetMap.HOT in example config (#26)
MapQuest Open Tiles service is announced to be discontinued and already started removing tiles at the highest zoom level. MapQuest provides a replacement service requiring custom javascript code to be injected, which I don't consider a good idea.
OpenStreetMap.HOT is a OpenStreetMap map style using pastel colours and thus is lot better than Mapnik for rendering stuff on it.
2016-05-22 03:12:08 +02:00
Milan Pässler 81379a8263 meshstats: fix NaN client count 2016-05-22 00:26:36 +02:00
Xaver Maierhofer 31d1464805 clean scss (#21)
* [TASK] CGL - Formation

* [TASK] Generic font fallback

* [TASK] Remove swp file
2016-05-19 17:47:32 +02:00
Xaver Maierhofer e1e510c308 use libsass (#22)
Avoid ruby dependency
2016-05-19 17:47:13 +02:00
PetaByteBoy // Milan Pässler ce8853c0fa fix examples 2016-05-09 18:17:45 +02:00
PetaByteBoy // Milan Pässler 075076a2fe infobox/*infos: fix iframe height option 2016-04-24 20:10:53 +02:00
Milan Pässler 2f28c51da1 forcegraph, main: l2tp support 2016-04-24 01:20:01 +02:00
Milan Pässler 2a78af4208 proportions: correct use of online vs all nodes 2016-04-22 01:05:32 +02:00
anoy 8497a5f833 reimplement feature 'encoded url' (#16)
change symbol names for better readability
beautify location-sidebar
cleanup sourcecode

* change symbols to unicode-names

* reimplement feature 'encoded url'
2016-04-21 22:34:21 +02:00
Marvin W 51c1c57e48 Save selected layer in local storage (#17) 2016-04-21 22:32:22 +02:00
eberhab bc7e7888a5 helper: str replace function should replace all instances of placeholder 2016-04-20 14:04:46 +02:00
anoy 31545c758d infobox/location: beautify location-sidebar
and cleanup sourcecode
2016-04-02 22:47:57 +02:00
45 changed files with 4324 additions and 740 deletions

16
.editorconfig Normal file
View file

@ -0,0 +1,16 @@
# EditorConfig is awesome: http://EditorConfig.org
# top-most EditorConfig file
root = true
charset = utf-8
# Get rid of whitespace to avoid diffs with a bunch of EOL changes
trim_trailing_whitespace = true
[*]
end_of_line = lf
insert_final_newline = true
[*.{js,html,scss,json}]
indent_size = 2
indent_style = space

View file

@ -1,7 +1,7 @@
language: node_js language: node_js
before_install: node_js:
- gem install sass - "lts/*"
- npm install -g grunt-cli - "node"
install: install:
- npm install - yarn
script: grunt script: node_modules/.bin/grunt

View file

@ -1,2 +0,0 @@
source 'https://rubygems.org'
gem "sass"

View file

@ -1,13 +0,0 @@
GEM
remote: https://rubygems.org/
specs:
sass (3.4.16)
PLATFORMS
ruby
DEPENDENCIES
sass
BUNDLED WITH
1.10.6

View file

@ -17,7 +17,7 @@ module.exports = function (grunt) {
grunt.loadTasks("tasks") grunt.loadTasks("tasks")
grunt.registerTask("default", ["bower-install-simple", "lint", "saveRevision", "copy", "sass", "requirejs"]) grunt.registerTask("default", ["lint", "saveRevision", "copy", "sass", "postcss", "requirejs"])
grunt.registerTask("lint", ["eslint"]) grunt.registerTask("lint", ["eslint"])
grunt.registerTask("dev", ["default", "connect:server", "watch"]) grunt.registerTask("dev", ["default", "connect:server", "watch"])
} }

View file

@ -1,8 +1,8 @@
[![Build Status](https://travis-ci.org/plumpudding/hopglass.svg?branch=master)](https://travis-ci.org/plumpudding/hopglass) [![Build Status](https://travis-ci.org/hopglass/hopglass.svg?branch=master)](https://travis-ci.org/hopglass/hopglass)
# HopGlass # HopGlass
HopGlass is a frontend for the [HopGlass Server](https://github.com/plumpudding/hopglass-server). HopGlass is a frontend for the [HopGlass Server](https://github.com/hopglass/hopglass-server).
# Screenshots # Screenshots
@ -14,31 +14,32 @@ HopGlass is a frontend for the [HopGlass Server](https://github.com/plumpudding/
# Dependencies # Dependencies
- npm - NodeJS
- bower - yarn (recommended) or npm
- grunt-cli
- Sass (>= 3.2)
# Installing dependencies # Installing dependencies
Install npm and Sass with your package-manager. On Debian-like systems run: Install npm package-manager. On Debian-like systems run:
sudo apt-get install npm ruby-sass sudo apt-get install nodejs
or if you have bundler you can install ruby-sass simply via `bundle install` **Note:** The official Debian packages for NodeJS are quite old, you might want to check at [NodeSource](https://github.com/nodesource/distributions/blob/master/README.md) for current binaries installable with your distribution's package manager.
On Mac you have to install only npm via brew and sass On Mac you can install nodejs and yarn via brew:
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
brew install node brew install node
sudo gem install sass brew install yarn
On Arch Linux you can install nodejs and yarn via pacman:
sudo pacman -S nodejs yarn
Execute these commands on your server as a normal user to prepare the dependencies: Execute these commands on your server as a normal user to prepare the dependencies:
git clone https://github.com/plumpudding/hopglass git clone https://github.com/hopglass/hopglass
cd hopglass cd hopglass
npm install yarn install # or `npm install`
npm install grunt-cli
# Building # Building
@ -54,7 +55,7 @@ Copy `config.json.example` to `build/config.json` and change it to match your co
## dataPath (string/array) ## dataPath (string/array)
`dataPath` can be either a string containing the address of a [HopGlass Server](https://github.com/plumpudding/hopglass-server) or an array containing multiple addresses. `dataPath` can be either a string containing the address of a [HopGlass Server](https://github.com/hopglass/hopglass-server) or an array containing multiple addresses.
Don't forget the trailing slash! Don't forget the trailing slash!
Also, proxying the data through a webserver will allow GZip and thus will greatly reduce bandwidth consumption. Also, proxying the data through a webserver will allow GZip and thus will greatly reduce bandwidth consumption.
It may help with firewall problems too. It may help with firewall problems too.
@ -119,12 +120,12 @@ Examples for `nodeInfos`:
{ "name": "Clientstatistik", { "name": "Clientstatistik",
"href": "stats/dashboard/db/node-byid?var-nodeid={NODE_ID}", "href": "stats/dashboard/db/node-byid?var-nodeid={NODE_ID}",
"thumbnail": "stats/render/dashboard-solo/db/node-byid?panelId=1&fullscreen&theme=light&width=600&height=300&var-nodeid={NODE_ID}" "thumbnail": "stats/render/dashboard-solo/db/node-byid?panelId=1&fullscreen&theme=light&width=600&height=300&var-nodeid={NODE_ID}",
"caption": "Knoten {NODE_ID}" "caption": "Knoten {NODE_ID}"
}, },
{ "name": "Uptime", { "name": "Uptime",
"href": "stats/dashboard/db/node-byid?var-nodeid={NODE_ID}", "href": "stats/dashboard/db/node-byid?var-nodeid={NODE_ID}",
"thumbnail": "stats/render/dashboard-solo/db/node-byid?panelId=2&fullscreen&theme=light&width=600&height=300&var-nodeid={NODE_ID}" "thumbnail": "stats/render/dashboard-solo/db/node-byid?panelId=2&fullscreen&theme=light&width=600&height=300&var-nodeid={NODE_ID}",
"caption": "Knoten {NODE_ID}" "caption": "Knoten {NODE_ID}"
} }
] ]

27
app.js
View file

@ -1,21 +1,20 @@
require.config({ require.config({
baseUrl: "lib", baseUrl: "lib",
paths: { paths: {
"leaflet": "../bower_components/leaflet/dist/leaflet", "leaflet": "../node_modules/leaflet/dist/leaflet",
"leaflet.label": "../bower_components/Leaflet.label/dist/leaflet.label", "leaflet.label": "../node_modules/leaflet-label/dist/leaflet.label",
"leaflet.providers": "../bower_components/leaflet-providers/leaflet-providers", "leaflet.providers": "../node_modules/leaflet-providers/leaflet-providers",
"chroma-js": "../bower_components/chroma-js/chroma.min", "chroma-js": "../node_modules/chroma-js/chroma.min",
"moment": "../bower_components/moment/min/moment-with-locales.min", "moment": "../node_modules/moment/min/moment-with-locales.min",
"tablesort": "../bower_components/tablesort/tablesort.min", "tablesort": "../node_modules/tablesort/tablesort.min",
"tablesort.numeric": "../bower_components/tablesort/src/sorts/tablesort.numeric", "tablesort.numeric": "../node_modules/tablesort/src/sorts/tablesort.numeric",
"d3": "../bower_components/d3/d3.min", "d3": "../node_modules/d3/d3.min",
"numeral": "../bower_components/numeraljs/min/numeral.min", "numeral": "../node_modules/numeraljs/min/numeral.min",
"numeral-intl": "../bower_components/numeraljs/min/languages.min", "numeral-intl": "../node_modules/numeraljs/min/languages.min",
"virtual-dom": "../bower_components/virtual-dom/dist/virtual-dom", "virtual-dom": "../node_modules/virtual-dom/dist/virtual-dom",
"rbush": "../bower_components/rbush/rbush", "rbush": "../node_modules/rbush/rbush",
"helper": "../helper", "helper": "../helper",
"jshashes": "../bower_components/jshashes/hashes", "jshashes": "../node_modules/jshashes/hashes"
"c3": "../bower_components/c3/c3.min"
}, },
shim: { shim: {
"leaflet.label": ["leaflet"], "leaflet.label": ["leaflet"],

View file

@ -1,40 +0,0 @@
{
"name": "HopGlass",
"ignore": [
"node_modules",
"bower_components",
"**/.*",
"test",
"tests"
],
"dependencies": {
"Leaflet.label": "~0.2.1",
"chroma-js": "~0.6.1",
"leaflet": "~0.7.3",
"ionicons": "~2.0.1",
"moment": "~2.9.0",
"requirejs": "~2.1.16",
"tablesort": "https://github.com/tristen/tablesort.git#v3.0.2",
"roboto-slab-fontface": "*",
"es6-shim": "~0.27.1",
"almond": "~0.3.1",
"r.js": "~2.1.16",
"d3": "~3.5.5",
"numeraljs": "~1.5.3",
"roboto-fontface": "~0.3.0",
"virtual-dom": "~2.0.1",
"leaflet-providers": "~1.0.27",
"rbush": "https://github.com/mourner/rbush.git#~1.3.5",
"jshashes": "~1.0.5",
"c3": "~0.4.10"
},
"authors": [
"Milan Pässler <me@petabyteboy.de>",
"Nils Schneider <nils@nilsschneider.net>"
],
"license": "AGPL3",
"private": true,
"resolutions": {
"d3": "~3.5.5"
}
}

View file

@ -1,6 +1,6 @@
({ ({
baseUrl: "lib", baseUrl: "lib",
name: "../bower_components/almond/almond", name: "../node_modules/almond/almond",
mainConfigFile: "app.js", mainConfigFile: "app.js",
include: "../app", include: "../app",
wrap: true, wrap: true,

View file

@ -1,62 +1,62 @@
{ {
"dataPath": [ "dataPath": "https://map.ffdus.de/data/",
"http://map.ffgl.eu/data/", "siteName": "Freifunk Flingern",
"http://karte.ffdus.de/data/",
"http://karte.freifunk-iserlohn.de/data/"
],
"siteName": "Freifunk Fluss",
"mapSigmaScale": 0.5, "mapSigmaScale": 0.5,
"showContact": true, "showContact": true,
"maxAge": 14, "maxAge": 14,
"mapLayers": [ "mapLayers": [
{ "name": "MapQuest", { "name": "CartoDB",
"url": "https://otile{s}-s.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.jpg", "url": "https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png",
"config": { "config": {
"subdomains": "1234", "maxZoom": 18,
"type": "osm", "attribution": "&copy; <a href=\"http://www.openstreetmap.org/copyright\">OpenStreetMap</a>, &copy; | <a href=\"https://carto.com/attribution\">CARTO</a>"
"attribution": "Tiles &copy; <a href=\"https://www.mapquest.com/\" target=\"_blank\">MapQuest</a>, Data CC-BY-SA OpenStreetMap",
"maxZoom": 18
} }
}, },
{ {
"name": "Stamen.TonerLite" "name": "OpenStreetMap.HOT"
},
{
"name": "Luftbilder NRW",
"url": "https://www.wms.nrw.de/geobasis/wms_nw_dop?",
"config": {
"maxZoom": 20,
"attribution": "<a href=\"http://www.bezreg-koeln.nrw.de/brk_internet/geobasis/luftbilderzeugnisse/digitale_orthophotos/index.html\">DOP20</a>, Land NRW (2017), Datenlizenz Deutschland - Namensnennung - Version 2.0 (<a href=\"https://www.govdata.de/dl-de/by-2-0\">www.govdata.de/dl-de/by-2-0</a>)",
"format": "image/jpeg",
"layers": "nw_dop_rgb"
}
}
],
"nodeInfos": [
{ "href": "https://map.eulenfunk.de/stats/dashboard/db/node-byid?var-nodeid={NODE_ID}",
"thumbnail": "https://map.eulenfunk.de/stats/render/dashboard-solo/db/node-byid?panelId=1&theme=light&width=600&height=300&var-nodeid={NODE_ID}"
},
{ "href": "https://map.eulenfunk.de/stats/dashboard/db/node-byid?var-nodeid={NODE_ID}",
"thumbnail": "https://map.eulenfunk.de/stats/render/dashboard-solo/db/node-byid?panelId=2&theme=light&width=600&height=500&var-nodeid={NODE_ID}"
},
{ "href": "https://map.eulenfunk.de/stats/dashboard/db/node-byid?var-nodeid={NODE_ID}",
"thumbnail": "https://map.eulenfunk.de/stats/render/dashboard-solo/db/node-byid?panelId=3&theme=light&width=600&height=200&var-nodeid={NODE_ID}"
}
],
"globalInfos": [
{ "href": "https://map.eulenfunk.de/stats/dashboard/db/global?var-job=dus",
"thumbnail": "https://map.eulenfunk.de/stats/render/dashboard-solo/db/global?panelId=1&&theme=light&width=800&height=600&var-job=dus"
},
{ "href": "https://map.eulenfunk.de/stats/dashboard/db/global?var-job=dus",
"thumbnail": "https://map.eulenfunk.de/stats/render/dashboard-solo/db/global?panelId=8&&theme=light&width=800&height=600&var-job=dus"
}
],
"linkInfos": [
{ "href": "https://map.eulenfunk.de/stats/dashboard/db/links?var-source={SOURCE}&var-target={TARGET}",
"thumbnail": "https://map.eulenfunk.de/stats/render/dashboard-solo/db/links?panelId=1&&theme=light&width=800&height=600&var-source={SOURCE}&var-target={TARGET}"
} }
], ],
"siteNames": [ "siteNames": [
{ "site": "ffgl-bcd", "name": "Burscheid" }, { "site": "dus", "name": "Flingern" }
{ "site": "ffgl-bgl", "name": "Bergisch Gladbach" },
{ "site": "ffgl-lln", "name": "Leichlingen" },
{ "site": "ffgl-ode", "name": "Odenthal" },
{ "site": "ffgl-ovr", "name": "Overath" },
{ "site": "ffgl-rrh", "name": "Rösrath" },
{ "site": "ffdus", "name": "Flingern" }
{ "site": "ffis", "name": "Iserlohn" }
], ],
"nodeCharts": [ "hwImg": [
{ { "thumbnail": "https://cdn.rawgit.com/Moorviper/meshviewer_hwpics/master/nodes/{MODELHASH}.svg",
"name": "Statistik", "caption": "Knoten {MODELHASH}"
"metrics": [
{
"id": "clients",
"color": "#1566A9",
"label": "Clients",
"query": "sum(statistics_clients_total{nodeid=%22{{NODE_ID}}%22})"
},
{
"id": "load",
"color": "1566A9",
"label": "Systemlast",
"query": "sum(statistics_loadavg{nodeid=%22{{NODE_ID}}%22})"
},
{
"id": "rx",
"color": "#1566A9",
"label": "Traffic (RX, kbps)",
"query": "sum(rate(statistics_traffic_rx_bytes{nodeid=%22{{NODE_ID}}%22}[30m])/125)"
}
],
"defaultMetric": "clients",
"url": "https://prometheus.map.eulenfunk.de/api/v1/query_range"
} }
] ]
} }

View file

@ -1,23 +1,23 @@
function get(url) { function get(url) {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
var req = new XMLHttpRequest(); var req = new XMLHttpRequest()
req.open('GET', url); req.open('GET', url)
req.onload = function() { req.onload = function() {
if (req.status == 200) { if (req.status == 200) {
resolve(req.response); resolve(req.response)
} }
else { else {
reject(Error(req.statusText)); reject(Error(req.statusText))
}
} }
};
req.onerror = function() { req.onerror = function() {
reject(Error("Network Error")); reject(Error("Network Error"))
}; }
req.send(); req.send()
}); })
} }
function getJSON(url) { function getJSON(url) {
@ -75,7 +75,8 @@ function localStorageTest() {
function listReplace(s, subst) { function listReplace(s, subst) {
for (key in subst) { for (key in subst) {
s = s.replace(key, subst[key]) var re = new RegExp(key, 'g')
s = s.replace(re, subst[key])
} }
return s return s
} }
@ -129,7 +130,13 @@ function attributeEntry(el, label, value) {
var tr = document.createElement("tr") var tr = document.createElement("tr")
var th = document.createElement("th") var th = document.createElement("th")
if (typeof label === "string")
th.textContent = label th.textContent = label
else {
th.appendChild(label)
tr.className = "routerpic"
}
tr.appendChild(th) tr.appendChild(th)
var td = document.createElement("td") var td = document.createElement("td")
@ -148,8 +155,8 @@ function attributeEntry(el, label, value) {
function createIframe(opt, width, height) { function createIframe(opt, width, height) {
el = document.createElement("iframe") el = document.createElement("iframe")
width = typeof width !== 'undefined' ? width : '525px'; width = typeof width !== 'undefined' ? width : '100%'
height = typeof height !== 'undefined' ? width : '350px'; height = typeof height !== 'undefined' ? height : '350px'
if (opt.src) if (opt.src)
el.src = opt.src el.src = opt.src
@ -179,7 +186,7 @@ function createIframe(opt, width, height) {
function showStat(o, subst) { function showStat(o, subst) {
var content, caption var content, caption
subst = typeof subst !== 'undefined' ? subst : {}; subst = typeof subst !== 'undefined' ? subst : {}
if (o.thumbnail) { if (o.thumbnail) {
content = document.createElement("img") content = document.createElement("img")
@ -194,7 +201,7 @@ function showStat(o, subst) {
} }
if (o.iframe) { if (o.iframe) {
content = createIframe(o.iframe) content = createIframe(o.iframe, o.width, o.height)
if (o.iframe.src) if (o.iframe.src)
content.src = listReplace(o.iframe.src, subst) content.src = listReplace(o.iframe.src, subst)
else else

View file

@ -4,9 +4,8 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no"> <meta name="viewport" content="width=device-width, user-scalable=no">
<link rel="stylesheet" href="css/ionicons.min.css"> <link rel="stylesheet" href="css/ionicons.min.css">
<link rel="stylesheet" href="roboto-slab-fontface.css"> <link rel="stylesheet" href="css/roboto-slab/roboto-slab-fontface.css">
<link rel="stylesheet" href="roboto-fontface.css"> <link rel="stylesheet" href="css/roboto/roboto-fontface.css">
<link rel="stylesheet" href="c3.min.css">
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<script src="vendor/es6-shim/es6-shim.min.js"></script> <script src="vendor/es6-shim/es6-shim.min.js"></script>
<script src="app.js"></script> <script src="app.js"></script>
@ -15,5 +14,15 @@
</script> </script>
</head> </head>
<body> <body>
<div class="loader">
<p>
Lade<br />
<span class="spinner"></span><br />
Karte &amp; Knoten...
</p>
<noscript>
<strong>JavaScript required</strong>
</noscript>
</div>
</body> </body>
</html> </html>

View file

@ -3,15 +3,14 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no"> <meta name="viewport" content="width=device-width, user-scalable=no">
<link rel="stylesheet" href="bower_components/roboto-slab-fontface/roboto-slab-fontface.css"> <link rel="stylesheet" href="node_modules/roboto-fontface/css/roboto-slab/roboto-slab-fontface.css">
<link rel="stylesheet" href="bower_components/roboto-fontface/roboto-fontface.css"> <link rel="stylesheet" href="node_modules/roboto-fontface/css/roboto/roboto-fontface.css">
<link rel="stylesheet" href="bower_components/leaflet/dist/leaflet.css"> <link rel="stylesheet" href="node_modules/leaflet/dist/leaflet.css">
<link rel="stylesheet" href="bower_components/Leaflet.label/dist/leaflet.label.css"> <link rel="stylesheet" href="node_modules/leaflet-label/dist/leaflet.label.css">
<link rel="stylesheet" href="bower_components/ionicons/css/ionicons.min.css"> <link rel="stylesheet" href="node_modules/ionicons/css/ionicons.min.css">
<link rel="stylesheet" href="bower_components/c3/c3.min.css">
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<script src="bower_components/es6-shim/es6-shim.min.js"></script> <script src="node_modules/es6-shim/es6-shim.min.js"></script>
<script src="bower_components/requirejs/require.js" data-main="app"></script> <script src="node_modules/requirejs/require.js" data-main="app"></script>
</head> </head>
<body> <body>
</body> </body>

View file

@ -29,8 +29,8 @@ define(function () {
s += "https://www.gnu.org/licenses/</a>.</p>" s += "https://www.gnu.org/licenses/</a>.</p>"
s += "<p>The source code is available at " s += "<p>The source code is available at "
s += "<a href=\"https://github.com/plumpudding/hopglass\">" s += "<a href=\"https://github.com/hopglass/hopglass\">"
s += "https://github.com/plumpudding/hopglass</a>." s += "https://github.com/hopglass/hopglass</a>."
el.innerHTML = s el.innerHTML = s
} }

View file

@ -4,9 +4,8 @@ define([], function () {
var n = Object.create(data) var n = Object.create(data)
n.nodes = {} n.nodes = {}
for (var key in data.nodes) { for (var key in data.nodes)
n.nodes[key] = data.nodes[key].filter(filter) n.nodes[key] = data.nodes[key].filter(filter)
}
var filteredIds = new Set() var filteredIds = new Set()

View file

@ -1,7 +1,7 @@
define(["d3"], function (d3) { define(["d3"], function (d3) {
var margin = 200 var margin = 200
var NODE_RADIUS = 15 var NODE_RADIUS = 15
var LINE_RADIUS = 12 var LINE_RADIUS = 7
return function (config, linkScale, sidebar, router) { return function (config, linkScale, sidebar, router) {
var self = this var self = this
@ -214,7 +214,7 @@ define(["d3"], function (d3) {
function drawLabel(d) { function drawLabel(d) {
var neighbours = d.neighbours.filter(function (d) { var neighbours = d.neighbours.filter(function (d) {
return d.link.o.type !== "VPN" return !d.link.o.isVPN
}) })
var sum = neighbours.reduce(function (a, b) { var sum = neighbours.reduce(function (a, b) {
@ -242,7 +242,8 @@ define(["d3"], function (d3) {
} }
function visibleLinks(d) { function visibleLinks(d) {
return (d.source.x > screenRect.left && d.source.x < screenRect.right && return (d.o.isVPN ||
d.source.x > screenRect.left && d.source.x < screenRect.right &&
d.source.y > screenRect.top && d.source.y < screenRect.bottom) || d.source.y > screenRect.top && d.source.y < screenRect.bottom) ||
(d.target.x > screenRect.left && d.target.x < screenRect.right && (d.target.x > screenRect.left && d.target.x < screenRect.right &&
d.target.y > screenRect.top && d.target.y < screenRect.bottom) d.target.y > screenRect.top && d.target.y < screenRect.bottom)
@ -324,16 +325,19 @@ define(["d3"], function (d3) {
links.forEach(function (d) { links.forEach(function (d) {
var dx = d.target.x - d.source.x var dx = d.target.x - d.source.x
var dy = d.target.y - d.source.y var dy = d.target.y - d.source.y
var a = Math.sqrt(dx * dx + dy * dy) var a = Math.sqrt(dx * dx + dy * dy) * 2
dx /= a dx /= a
dy /= a dy /= a
var distancex = d.target.x - d.source.x - (10 * dx)
var distancey = d.target.y - d.source.y - (10 * dy)
ctx.beginPath() ctx.beginPath()
ctx.moveTo(d.source.x + dx * nodeRadius, d.source.y + dy * nodeRadius) ctx.moveTo(d.source.x + dx * nodeRadius, d.source.y + dy * nodeRadius)
ctx.lineTo(d.target.x - dx * nodeRadius, d.target.y - dy * nodeRadius) ctx.lineTo(d.target.x - (distancex / 2) - dx * nodeRadius, d.target.y - (distancey / 2) - dy * nodeRadius)
ctx.strokeStyle = d.o.type === "Kabel" ? cableColor : d.color ctx.strokeStyle = d.o.type === "Kabel" ? cableColor : d.color
ctx.globalAlpha = d.o.type === "VPN" ? 0.1 : 0.8 ctx.globalAlpha = d.o.isVPN ? 0.1 : 0.8
ctx.lineWidth = d.o.type === "VPN" ? 1.5 : 2.5 ctx.lineWidth = d.o.isVPN ? 1.5 : 2.5
ctx.stroke() ctx.stroke()
}) })
@ -376,6 +380,7 @@ define(["d3"], function (d3) {
// -- draw clients -- // -- draw clients --
ctx.save() ctx.save()
ctx.beginPath() ctx.beginPath()
if (scale > 0.9)
nodes.filter(visibleNodes).forEach(function (d) { nodes.filter(visibleNodes).forEach(function (d) {
var clients = d.o.node.statistics.clients var clients = d.o.node.statistics.clients
if (clients === 0) if (clients === 0)
@ -470,32 +475,34 @@ define(["d3"], function (d3) {
requestAnimationFrame(redraw) requestAnimationFrame(redraw)
} }
function distance(a, b) { function distance(ax, ay, bx, by) {
return Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2) return Math.pow(ax - bx, 2) + Math.pow(ay - by, 2)
} }
function distancePoint(a, b) { function distancePoint(a, b) {
return Math.sqrt(distance(a, b)) return Math.sqrt(distance(a.x, a.y, b.x, b.y))
} }
function distanceLink(p, a, b) { function distanceLink(p, a, b) {
/* http://stackoverflow.com/questions/849211 */ /* http://stackoverflow.com/questions/849211 */
var l2 = distance(a, b) var bx = b.x - ((b.x - a.x) / 2)
var by = b.y - ((b.y - a.y) / 2)
var l2 = distance(a.x, a.y, bx, by)
if (l2 === 0) if (l2 === 0)
return distance(p, a) return distance(p.x, p.y, a.x, a.y)
var t = ((p.x - a.x) * (b.x - a.x) + (p.y - a.y) * (b.y - a.y)) / l2 var t = ((p.x - a.x) * (bx - a.x) + (p.y - a.y) * (by - a.y)) / l2
if (t < 0) if (t < 0)
return distance(p, a) return distance(p.x, p.y, a.x, a.y)
if (t > 1) if (t > 1)
return distance(p, b) return distance(p.x, p.y, bx, by)
return Math.sqrt(distance(p, { x: a.x + t * (b.x - a.x), return Math.sqrt(distance(p.x, p.y, a.x + t * (bx - a.x), a.y + t * (by - a.y) ))
y: a.y + t * (b.y - a.y) }))
} }
function translateXY(d) { function translateXY(d) {
@ -523,7 +530,7 @@ define(["d3"], function (d3) {
} }
var links = intLinks.filter(function (d) { var links = intLinks.filter(function (d) {
return d.o.type !== "VPN" return !d.o.isVPN
}).filter(function (d) { }).filter(function (d) {
return distanceLink(e, d.source, d.target) < LINE_RADIUS return distanceLink(e, d.source, d.target) < LINE_RADIUS
}) })
@ -584,13 +591,13 @@ define(["d3"], function (d3) {
.charge(-250) .charge(-250)
.gravity(0.1) .gravity(0.1)
.linkDistance(function (d) { .linkDistance(function (d) {
if (d.o.type === "VPN") if (d.o.isVPN)
return 0 return 0
else else
return LINK_DISTANCE return LINK_DISTANCE
}) })
.linkStrength(function (d) { .linkStrength(function (d) {
if (d.o.type === "VPN") if (d.o.isVPN)
return 0 return 0
else else
return Math.max(0.5, 1 / d.o.tq) return Math.max(0.5, 1 / d.o.tq)
@ -644,7 +651,7 @@ define(["d3"], function (d3) {
e.source = newNodesDict[d.source.id] e.source = newNodesDict[d.source.id]
e.target = newNodesDict[d.target.id] e.target = newNodesDict[d.target.id]
if (d.type === "VPN") if (d.isVPN)
e.color = "rgba(255, 255, 255, " + (0.6 / d.tq) + ")" e.color = "rgba(255, 255, 255, " + (0.6 / d.tq) + ")"
else else
e.color = linkScale(d.tq).hex() e.color = linkScale(d.tq).hex()
@ -696,6 +703,15 @@ define(["d3"], function (d3) {
linksDict[d.o.id] = d linksDict[d.o.id] = d
}) })
intLinks.forEach(function (d) {
if (linksDict[d.target.o.node_id + "-" + d.source.o.node_id])
return
var obj = { source: d.target, target: d.source, o: { isVPN: d.o.isVPN, type: "dead", id: d.target.o.node_id + "-" + d.source.o.node_id, tq: 1 }, color: "rgba(255, 255, 255, 0.6)" }
intLinks.push(obj)
linksDict[d.target.o.node_id + "-" + d.source.o.node_id] = obj
})
intNodes.forEach(function (d) { intNodes.forEach(function (d) {
d.neighbours = Object.keys(d.neighbours).map(function (k) { d.neighbours = Object.keys(d.neighbours).map(function (k) {
return d.neighbours[k] return d.neighbours[k]

View file

@ -48,6 +48,9 @@ function (chroma, Map, Sidebar, Tabs, Container, Meshstats, Legend, Linklist,
} }
} }
var loader = document.getElementsByClassName("loader")[0]
loader.classList.add("hide")
contentDiv = document.createElement("div") contentDiv = document.createElement("div")
contentDiv.classList.add("content") contentDiv.classList.add("content")
document.body.appendChild(contentDiv) document.body.appendChild(contentDiv)
@ -57,7 +60,7 @@ function (chroma, Map, Sidebar, Tabs, Container, Meshstats, Legend, Linklist,
contentDiv.appendChild(buttons) contentDiv.appendChild(buttons)
var buttonToggle = document.createElement("button") var buttonToggle = document.createElement("button")
buttonToggle.textContent = "" buttonToggle.textContent = "\uF133"
buttonToggle.onclick = function () { buttonToggle.onclick = function () {
if (content.constructor === Map) if (content.constructor === Map)
router.view("g") router.view("g")

View file

@ -1,255 +0,0 @@
define(["c3", "d3"], function (c3, d3) {
var charts = function (node, config) {
this.node = node
this.chartConfig = config
this.chart = null
this.zoomConfig = {
"levels": [
{"label": "8h", "from": 8 * 3600, "interval": 15 * 60}
/*{"label": "24h", "from": "26h", "interval": "1h"},
{"label": "1m", "from": "1mon", "interval": "1d"},
{"label": "1y", "from": 31536000, "interval": 2628000}*/
]
}
this.zoomLevel = 0
this.c3Config = {
"size": {
"height": 240
},
padding: {
bottom: 30
},
"legend": {
"item": {
"onclick": function (id) {
this.api.hide()
this.api.show(id)
}
}
},
"tooltip": {
"format": {
"value": this.c3FormatToolTip.bind(this)
}
},
"axis": {
"x": {
"type": "timeseries",
"tick": {
"format": this.c3FormatXAxis.bind(this),
"rotate": -45
}
},
"y": {
"min": 0,
"padding": {
"bottom": 0
}
}
}
}
this.cache = []
this.init()
}
charts.prototype = {
init: function () {
// Workaround for endless loop bug
if (this.c3Config.axis.x.tick && this.c3Config.axis.x.tick.format && typeof this.c3Config.axis.x.tick.format === "function") {
if (this.c3Config.axis.x.tick.format && !this.c3Config.axis.x.tick._format)
this.c3Config.axis.x.tick._format = this.c3Config.axis.x.tick.format
this.c3Config.axis.x.tick.format = function (val) {
return this.c3Config.axis.x.tick._format(val)
}.bind(this)
}
// Configure metrics
this.c3Config.data = {
"keys": {
"x": "time",
"value": this.chartConfig.metrics.map(function (metric) {
return metric.id
})
},
"colors": this.chartConfig.metrics.reduce(function (collector, metric) {
collector[metric.id] = metric.color
return collector
}, {}),
"names": this.chartConfig.metrics.reduce(function (collector, metric) {
collector[metric.id] = metric.label
return collector
}, {}),
"hide": this.chartConfig.metrics.map(function (metric) {
return metric.id
}).filter(function (id) {
return id !== this.chartConfig.defaultMetric
}.bind(this))
}
},
render: function () {
var div = document.createElement("div")
div.classList.add("chart")
var h4 = document.createElement("h4")
h4.textContent = this.chartConfig.name
div.appendChild(h4)
// Render chart
this.load(function (data) {
div.appendChild(this.renderChart(data))
// Render zoom controls
if (this.zoomConfig.levels.length > 0)
div.appendChild(this.renderZoomControls())
}.bind(this))
return div
},
renderChart: function (data) {
this.c3Config.data.json = data
this.chart = c3.generate(this.c3Config)
return this.chart.element
},
updateChart: function (data) {
this.c3Config.data.json = data
this.chart.load(this.c3Config.data)
},
renderZoomControls: function () {
// Draw zoom controls
var zoomDiv = document.createElement("div")
zoomDiv.classList.add("zoom-buttons")
var zoomButtons = []
this.zoomConfig.levels.forEach(function (v, level) {
var btn = document.createElement("button")
btn.classList.add("zoom-button")
btn.setAttribute("data-zoom-level", level)
if (level === this.zoomLevel)
btn.classList.add("active")
btn.onclick = function () {
if (level !== this.zoomLevel) {
zoomButtons.forEach(function (v, k) {
if (level !== k)
v.classList.remove("active")
else
v.classList.add("active")
})
this.setZoomLevel(level)
}
}.bind(this)
btn.textContent = v.label
zoomButtons[level] = btn
zoomDiv.appendChild(btn)
}.bind(this))
return zoomDiv
},
setZoomLevel: function (level) {
if (level !== this.zoomLevel) {
this.zoomLevel = level
this.load(this.updateChart.bind(this))
}
},
load: function (callback) {
if (this.cache[this.zoomLevel])
callback(this.cache[this.zoomLevel])
else {
var urls = []
var id = this.node.nodeinfo.node_id
var unixStamp = Math.floor(Date.now() / 1000)
var zoomConfig = this.zoomConfig.levels[this.zoomLevel]
this.chartConfig.metrics.forEach(function(metric) {
var parameters = [
"start=" + (unixStamp - zoomConfig.from),
"end=" + unixStamp,
"step=" + zoomConfig.interval,
"query=" + metric.query.replace("{{NODE_ID}}", id)
]
var url = this.chartConfig.url + "?" + parameters.join("&")
urls.push(url)
}.bind(this))
Promise.all(urls.map(getJSON)).then(function (data) {
this.cache[this.zoomLevel] = this.parse(data)
callback(this.cache[this.zoomLevel])
}.bind(this))
}
},
parse: function (results) {
var data = []
results[0].data.result[0].values.forEach(function (tp) {
var time = {"time": new Date(tp[0] * 1000)}
results.forEach(function(result) {
var metric = this.chartConfig.metrics[results.indexOf(result)]
var index = results[0].data.result[0].values.indexOf(tp)
time[metric.id] = this.formatValue(metric.id, result.data.result[0].values[index][1])
}.bind(this))
data.push(time)
}.bind(this))
return data
},
c3FormatToolTip: function (d, ratio, id) {
switch (id) {
case "uptime":
return d.toFixed(1) + " Tage"
default:
return d
}
},
c3FormatXAxis: function (d) {
var pad = function (number, pad) {
var N = Math.pow(10, pad)
return number < N ? ("" + (N + number)).slice(1) : "" + number
}
switch (this.zoomLevel) {
case 0: // 8h
case 1: // 24h
return pad(d.getHours(), 2) + ":" + pad(d.getMinutes(), 2)
case 2: // 1m
return pad(d.getDate(), 2) + "." + pad(d.getMonth() + 1, 2)
case 3: // 1y
return ["Jan", "Feb", "Mrz", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"][d.getMonth()]
default:
break
}
},
formatValue: function (id, value) {
switch (id) {
case "loadavg":
return (d3.format(".2r")(value))
case "clientcount":
return (Math.ceil(value))
case "uptime":
return (value / 86400)
default:
return value
}
}
}
return charts
})

View file

@ -1,8 +1,10 @@
define(function () { define(function () {
function showStatImg(o, source, target) { function showStatImg(o, d) {
var subst = {} var subst = {}
subst["{SOURCE}"] = source subst["{SOURCE}"] = d.source.node_id
subst["{TARGET}"] = target subst["{SOURCE_NAME}"] = d.source.node.nodeinfo.hostname ? d.source.node.nodeinfo.hostname : "unknown"
subst["{TARGET}"] = d.target.node_id
subst["{TARGET_NAME}"] = d.target.node.nodeinfo.hostname ? d.target.node.nodeinfo.hostname : "unknown"
return showStat(o, subst) return showStat(o, subst)
} }
@ -35,15 +37,12 @@ define(function () {
attributeEntry(attributes, "Hardware", (hw1 != null ? hw1 : "unbekannt") + " " + (hw2 != null ? hw2 : "unbekannt")) attributeEntry(attributes, "Hardware", (hw1 != null ? hw1 : "unbekannt") + " " + (hw2 != null ? hw2 : "unbekannt"))
el.appendChild(attributes) el.appendChild(attributes)
if (config.linkInfos) { if (config.linkInfos)
var source = d.source.node_id
var target = d.target.node_id
config.linkInfos.forEach( function (linkInfo) { config.linkInfos.forEach( function (linkInfo) {
var h4 = document.createElement("h4") var h4 = document.createElement("h4")
h4.textContent = linkInfo.name h4.textContent = linkInfo.name
el.appendChild(h4) el.appendChild(h4)
el.appendChild(showStatImg(linkInfo, source, target)) el.appendChild(showStatImg(linkInfo, d))
}) })
} }
}
}) })

View file

@ -1,80 +1,83 @@
define(function () { define(function () {
return function (config, el, router, d) { return function (config, el, router, d) {
var h2 = document.createElement("h2") var sidebarTitle = document.createElement("h2")
h2.textContent = "Location: " + d.toString() sidebarTitle.textContent = "Location: " + d.toString()
el.appendChild(h2) el.appendChild(sidebarTitle)
getJSON("https://nominatim.openstreetmap.org/reverse?format=json&lat=" + d.lat + "&lon=" + d.lng + "&zoom=18&addressdetail=0") getJSON("https://nominatim.openstreetmap.org/reverse?format=json&lat=" + d.lat + "&lon=" + d.lng + "&zoom=18&addressdetails=0")
.then(function(result) { .then(function(result) {
h2.textContent = result.display_name if(result.display_name)
sidebarTitle.textContent = result.display_name
}) })
var latDiv = document.createElement("div") var editLat = document.createElement("input")
var h3lat = document.createElement("h3") editLat.type = "text"
h3lat.textContent = "Breitengrad" editLat.value = d.lat.toFixed(9)
h3lat.id = "h3-latitude" el.appendChild(createBox("lat", "Breitengrad", editLat))
latDiv.appendChild(h3lat)
var txt1 = document.createElement("textarea")
txt1.id = "location-latitude"
txt1.value = d.lat.toFixed(9)
var p = document.createElement("p")
p.appendChild(txt1)
p.appendChild(createCopyButton(txt1.id))
latDiv.appendChild(p)
el.appendChild(latDiv)
var longDiv = document.createElement("div") var editLng = document.createElement("input")
var h3lng = document.createElement("h3") editLng.type = "text"
h3lng.textContent = "Längengrad" editLng.value = d.lng.toFixed(9)
longDiv.appendChild(h3lng) el.appendChild(createBox("lng", "Längengrad", editLng))
var txt2 = document.createElement("textarea")
txt2.id = "location-longitude"
txt2.value = d.lng.toFixed(9)
var p2 = document.createElement("p")
p2.appendChild(txt2)
p2.appendChild(createCopyButton(txt2.id))
longDiv.appendChild(p2)
longDiv.id = "div-longitude"
el.appendChild(longDiv)
var a1 = document.createElement("a") var editUci = document.createElement("textarea")
a1.textContent = "plain" editUci.value =
a1.onclick = function() { "uci set gluon-node-info.@location[0]='location'; " +
"uci set gluon-node-info.@location[0].share_location='1';" +
"uci set gluon-node-info.@location[0].latitude='" + d.lat.toFixed(9) + "';" +
"uci set gluon-node-info.@location[0].longitude='" + d.lng.toFixed(9) + "';" +
"uci commit gluon-node-info"
el.appendChild(createBox("uci", "Befehl", editUci, false))
var linkPlain = document.createElement("a")
linkPlain.textContent = "plain"
linkPlain.onclick = function() {
switch2plain() switch2plain()
return false return false
} }
a1.href = "#" linkPlain.href = "#"
var a2 = document.createElement("a")
a2.textContent = "uci" var linkUci = document.createElement("a")
a2.onclick = function() { linkUci.textContent = "uci"
linkUci.onclick = function() {
switch2uci() switch2uci()
return false return false
} }
a2.href = "#" linkUci.href = "#"
var p3 = document.createElement("p") var hintText = document.createElement("p")
p3.textContent = "Du kannst zwischen " hintText.appendChild(document.createTextNode("Du kannst zwischen "))
p3.appendChild(a1) hintText.appendChild(linkPlain)
var t1 = document.createTextNode(" und ") hintText.appendChild(document.createTextNode(" und "))
p3.appendChild(t1) hintText.appendChild(linkUci)
p3.appendChild(a2) hintText.appendChild(document.createTextNode(" wechseln."))
var t2 = document.createTextNode(" wechseln.") el.appendChild(hintText)
p3.appendChild(t2)
el.appendChild(p3)
function createCopyButton(id) { function createBox(name, title, inputElem, isVisible) {
var visible = typeof isVisible !== "undefined" ? isVisible : true
var box = document.createElement("div")
var heading = document.createElement("h3")
heading.textContent = title
box.appendChild(heading)
var btn = document.createElement("button") var btn = document.createElement("button")
btn.className = "ion-ios-copy" btn.className = "ion-ios-copy"
btn.title = "Kopieren" btn.title = "Kopieren"
btn.onclick = function() { btn.onclick = function() { copy2clip(inputElem.id) }
copy2clip(id) inputElem.id = "location-" + name
} inputElem.readOnly = true
return btn var line = document.createElement("p")
line.appendChild(inputElem)
line.appendChild(btn)
box.appendChild(line)
box.id = "box-" + name
box.style.display = visible ? "block" : "none"
return box
} }
function copy2clip(id) { function copy2clip(id) {
var copyTextarea = document.querySelector("#" + id) var copyField = document.querySelector("#" + id)
copyTextarea.select() copyField.select()
try { try {
document.execCommand("copy") document.execCommand("copy")
} catch (err) { } catch (err) {
@ -83,18 +86,15 @@ define(function () {
} }
function switch2plain() { function switch2plain() {
var box1 = document.getElementById("location-latitude") document.getElementById("box-uci").style.display = "none"
box1.value = d.lat.toFixed(9) document.getElementById("box-lat").style.display = "block"
var box2 = document.getElementById("location-longitude") document.getElementById("box-lng").style.display = "block"
box2.value = d.lng.toFixed(9)
document.getElementById("h3-latitude").textContent = "Breitengrad"
document.getElementById("div-longitude").style.display = "block"
} }
function switch2uci() { function switch2uci() {
document.getElementById("location-latitude").value = "uci set gluon-node-info.@location[0]='location'; uci set gluon-node-info.@location[0].share_location='1'; uci set gluon-node-info.@location[0].latitude='" + d.lat.toFixed(9) + "'; uci set gluon-node-info.@location[0].longitude='" + d.lng.toFixed(9) + "'; uci commit gluon-node-info" document.getElementById("box-uci").style.display = "block"
document.getElementById("h3-latitude").textContent = "Befehl" document.getElementById("box-lat").style.display = "none"
document.getElementById("div-longitude").style.display = "none" document.getElementById("box-lng").style.display = "none"
} }
} }
}) })

View file

@ -1,5 +1,5 @@
define(["moment", "numeral", "tablesort", "infobox/charts", "tablesort.numeric"], define(["moment", "numeral", "tablesort", "tablesort.numeric"],
function (moment, numeral, Tablesort, Charts) { function (moment, numeral, Tablesort) {
function showGeoURI(d) { function showGeoURI(d) {
function showLatitude(d) { function showLatitude(d) {
var suffix = Math.sign(d) > -1 ? "'N" : "'S" var suffix = Math.sign(d) > -1 ? "'N" : "'S"
@ -81,18 +81,124 @@ define(["moment", "numeral", "tablesort", "infobox/charts", "tablesort.numeric"]
return d.firstseen.fromNow(true) return d.firstseen.fromNow(true)
} }
function wifiChannelAlias(ch) {
var chlist = {
"1": "2412 MHz",
"2": "2417 MHz",
"3": "2422 MHz",
"4": "2427 MHz",
"5": "2432 MHz",
"6": "2437 MHz",
"7": "2442 MHz",
"8": "2447 MHz",
"9": "2452 MHz",
"10": "2457 MHz",
"11": "2462 MHz",
"12": "2467 MHz",
"13": "2472 MHz",
"36": "5180 MHz (Indoors)",
"40": "5200 MHz (Indoors)",
"44": "5220 MHz (Indoors)",
"48": "5240 MHz (Indoors)",
"52": "5260 MHz (Indoors/DFS/TPC)",
"56": "5280 MHz (Indoors/DFS/TPC)",
"60": "5300 MHz (Indoors/DFS/TPC)",
"64": "5320 MHz (Indoors/DFS/TPC)",
"100": "5500 MHz (DFS) !!",
"104": "5520 MHz (DFS) !!",
"108": "5540 MHz (DFS) !!",
"112": "5560 MHz (DFS) !!",
"116": "5580 MHz (DFS) !!",
"120": "5600 MHz (DFS) !!",
"124": "5620 MHz (DFS) !!",
"128": "5640 MHz (DFS) !!",
"132": "5660 MHz (DFS) !!",
"136": "5680 MHz (DFS) !!",
"140": "5700 MHz (DFS) !!"
}
if (!(ch in chlist))
return ""
else
return chlist[ch]
}
function showWifiChannel(ch) {
if (!ch)
return undefined
return ch + " (" + wifiChannelAlias(ch) + ")"
}
function showClients(d) { function showClients(d) {
if (!d.flags.online) if (!d.flags.online)
return undefined return undefined
var meshclients = getMeshClients(d)
resetMeshClients(d)
var before = " ("
var after = " in der lokalen Wolke)"
return function (el) { return function (el) {
el.appendChild(document.createTextNode(d.statistics.clients > 0 ? d.statistics.clients : "keine")) el.appendChild(document.createTextNode(d.statistics.clients > 0 ? d.statistics.clients : "keine"))
el.appendChild(document.createTextNode(before))
el.appendChild(document.createTextNode(meshclients > 0 ? meshclients : "keine"))
el.appendChild(document.createTextNode(after))
el.appendChild(document.createElement("br")) el.appendChild(document.createElement("br"))
var span = document.createElement("span") var span = document.createElement("span")
span.classList.add("clients") span.classList.add("clients")
span.textContent = " ".repeat(d.statistics.clients) span.textContent = " ".repeat(d.statistics.clients)
el.appendChild(span) el.appendChild(span)
var spanmesh = document.createElement("span")
spanmesh.classList.add("clientsMesh")
spanmesh.textContent = " ".repeat(meshclients - d.statistics.clients)
el.appendChild(spanmesh)
}
}
function getMeshClients(node) {
var meshclients = 0
if (node.statistics && !isNaN(node.statistics.clients))
meshclients = node.statistics.clients
if (!node)
return 0
if (node.parsed)
return 0
node.parsed = 1
node.neighbours.forEach(function (neighbour) {
if (!neighbour.link.isVPN && neighbour.node)
meshclients += getMeshClients(neighbour.node)
})
return meshclients
}
function resetMeshClients(node) {
if (!node.parsed)
return
node.parsed = 0
node.neighbours.forEach(function (neighbour) {
if (!neighbour.link.isVPN && neighbour.node)
resetMeshClients(neighbour.node)
})
return
}
function showMeshClients(d) {
if (!d.flags.online)
return undefined
var meshclients = getMeshClients(d)
resetMeshClients(d)
return function (el) {
el.appendChild(document.createTextNode(meshclients > 0 ? meshclients : "keine"))
el.appendChild(document.createElement("br"))
} }
} }
@ -112,6 +218,9 @@ define(["moment", "numeral", "tablesort", "infobox/charts", "tablesort.numeric"]
if (link) { if (link) {
var a = document.createElement("a") var a = document.createElement("a")
if (ip.includes("."))
a.href = "http://" + ip + "/"
else
a.href = "http://[" + ip + "]/" a.href = "http://[" + ip + "]/"
a.textContent = ip a.textContent = ip
el.appendChild(a) el.appendChild(a)
@ -156,7 +265,7 @@ define(["moment", "numeral", "tablesort", "infobox/charts", "tablesort.numeric"]
} }
var label = document.createElement("label") var label = document.createElement("label")
label.textContent = (v) label.textContent = +(Math.round(v + "e+2") + "e-2")
span.appendChild(label) span.appendChild(label)
return span return span
@ -180,6 +289,65 @@ define(["moment", "numeral", "tablesort", "infobox/charts", "tablesort.numeric"]
} }
} }
function showAirtime(band, val) {
if (!val)
return undefined
return function (el) {
el.appendChild(showBar("airtime" + band.toString(), val))
}
}
function createLink(target, router) {
if (!target) return document.createTextNode("unknown")
var unknown = !(target.node)
var text = unknown ? (target.id ? target.id : target) : target.node.nodeinfo.hostname
if (!unknown) {
var link = document.createElement("a")
link.classList.add("hostname-link")
link.href = "#"
link.onclick = router.node(target.node)
link.textContent = text
return link
}
return document.createTextNode(text)
}
function showGateway(d, router) {
var nh
if (dictGet(d.statistics, ["nexthop"]))
nh = dictGet(d.statistics, ["nexthop"])
if (dictGet(d.statistics, ["gateway_nexthop"]))
nh = dictGet(d.statistics, ["gateway_nexthop"])
var gw = dictGet(d.statistics, ["gateway"])
if (!gw) return null
return function (el) {
var num = 0
while (gw && nh && gw.id !== nh.id && num < 10) {
if (num !== 0) el.appendChild(document.createTextNode(" -> "))
el.appendChild(createLink(nh, router))
num++
if (!nh.node || !nh.node.statistics) break
if (!dictGet(nh.node.statistics, ["gateway"]) || !dictGet(nh.node.statistics, ["gateway"]).id) break
if (dictGet(nh.node.statistics, ["gateway"]).id !== gw.id) break
if (dictGet(nh.node.statistics, ["gateway_nexthop"]))
nh = dictGet(nh.node.statistics, ["gateway_nexthop"])
else if (dictGet(nh.node.statistics, ["nexthop"]))
nh = dictGet(nh.node.statistics, ["nexthop"])
else
break
}
if (gw && nh && gw.id !== nh.id) {
if (num !== 0) el.appendChild(document.createTextNode(" -> "))
num++
el.appendChild(document.createTextNode("..."))
}
if (num !== 0) el.appendChild(document.createTextNode(" -> "))
el.appendChild(createLink(gw, router))
}
}
function showPages(d) { function showPages(d) {
var webpages = dictGet(d.nodeinfo, ["pages"]) var webpages = dictGet(d.nodeinfo, ["pages"])
if (webpages === null) if (webpages === null)
@ -219,6 +387,37 @@ define(["moment", "numeral", "tablesort", "infobox/charts", "tablesort.numeric"]
return au.enabled ? "aktiviert (" + au.branch + ")" : "deaktiviert" return au.enabled ? "aktiviert (" + au.branch + ")" : "deaktiviert"
} }
function showNodeImg(o, model) {
if (!model)
return document.createTextNode("Knotenname")
var content, caption
var modelhash = model.split("").reduce(function(a, b) {
a = ((a << 5) - a) + b.charCodeAt(0)
return a & a
}, 0)
content = document.createElement("img")
content.id = "routerpicture"
content.classList.add("nodeImg")
content.src = o.thumbnail.replace("{MODELHASH}", modelhash)
content.onerror = function() {
document.getElementById("routerpicdiv").outerHTML = "Knotenname"
}
if (o.caption) {
caption = o.caption.replace("{MODELHASH}", modelhash)
if (!content)
content = document.createTextNode(caption)
}
var p = document.createElement("p")
p.appendChild(content)
return content
}
function showStatImg(o, d) { function showStatImg(o, d) {
var subst = {} var subst = {}
subst["{NODE_ID}"] = d.nodeinfo.node_id ? d.nodeinfo.node_id : "unknown" subst["{NODE_ID}"] = d.nodeinfo.node_id ? d.nodeinfo.node_id : "unknown"
@ -227,12 +426,29 @@ define(["moment", "numeral", "tablesort", "infobox/charts", "tablesort.numeric"]
} }
return function(config, el, router, d) { return function(config, el, router, d) {
var attributes = document.createElement("table")
attributes.classList.add("attributes")
if (config.hwImg) {
var top = document.createElement("div")
top.id = "routerpicdiv"
try {
config.hwImg.forEach(function(hwImg) {
try {
top.appendChild(showNodeImg(hwImg, dictGet(d, ["nodeinfo", "hardware", "model"])))
} catch (err) {
console.log(err.message)
}
})
} catch (err) {
console.log(err.message)
}
attributeEntry(attributes, top, d.nodeinfo.hostname)
} else {
var h2 = document.createElement("h2") var h2 = document.createElement("h2")
h2.textContent = d.nodeinfo.hostname h2.textContent = d.nodeinfo.hostname
el.appendChild(h2) el.appendChild(h2)
}
var attributes = document.createElement("table")
attributes.classList.add("attributes")
attributeEntry(attributes, "Status", showStatus(d)) attributeEntry(attributes, "Status", showStatus(d))
attributeEntry(attributes, "Gateway", d.flags.gateway ? "ja" : null) attributeEntry(attributes, "Gateway", d.flags.gateway ? "ja" : null)
@ -248,20 +464,20 @@ define(["moment", "numeral", "tablesort", "infobox/charts", "tablesort.numeric"]
attributeEntry(attributes, "Site", showSite(d, config)) attributeEntry(attributes, "Site", showSite(d, config))
attributeEntry(attributes, "Uptime", showUptime(d)) attributeEntry(attributes, "Uptime", showUptime(d))
attributeEntry(attributes, "Teil des Netzes", showFirstseen(d)) attributeEntry(attributes, "Teil des Netzes", showFirstseen(d))
attributeEntry(attributes, "Kanal 2.4 GHz", showWifiChannel(dictGet(d.nodeinfo, ["wireless", "chan2"])))
attributeEntry(attributes, "Kanal 5 GHz", showWifiChannel(dictGet(d.nodeinfo, ["wireless", "chan5"])))
attributeEntry(attributes, "Airtime 2.4 GHz", showAirtime(2, dictGet(d.statistics, ["wireless", "airtime2"])))
attributeEntry(attributes, "Airtime 5 GHz", showAirtime(5, dictGet(d.statistics, ["wireless", "airtime5"])))
attributeEntry(attributes, "Systemlast", showLoad(d)) attributeEntry(attributes, "Systemlast", showLoad(d))
attributeEntry(attributes, "Arbeitsspeicher", showRAM(d)) attributeEntry(attributes, "Arbeitsspeicher", showRAM(d))
attributeEntry(attributes, "IP Adressen", showIPs(d)) attributeEntry(attributes, "IP Adressen", showIPs(d))
attributeEntry(attributes, "Webseite", showPages(d)) attributeEntry(attributes, "Webseite", showPages(d))
attributeEntry(attributes, "Gewähltes Gateway", dictGet(d.statistics, ["gateway"])) attributeEntry(attributes, "Gewähltes Gateway", showGateway(d, router))
attributeEntry(attributes, "Autom. Updates", showAutoupdate(d)) attributeEntry(attributes, "Autom. Updates", showAutoupdate(d))
attributeEntry(attributes, "Clients", showClients(d)) attributeEntry(attributes, "Clients", showClients(d), showMeshClients(d))
el.appendChild(attributes) el.appendChild(attributes)
if (!d.flags.gateway && config.nodeCharts)
config.nodeCharts.forEach( function (config) {
el.appendChild((new Charts(d, config)).render())
})
if (config.nodeInfos) if (config.nodeInfos)
config.nodeInfos.forEach( function (nodeInfo) { config.nodeInfos.forEach( function (nodeInfo) {
@ -315,13 +531,7 @@ define(["moment", "numeral", "tablesort", "infobox/charts", "tablesort.numeric"]
tr.appendChild(td1) tr.appendChild(td1)
var td2 = document.createElement("td") var td2 = document.createElement("td")
var a1 = document.createElement("a") td2.appendChild(createLink(d, router))
a1.classList.add("hostname")
a1.textContent = unknown ? d.id : d.node.nodeinfo.hostname
if (!unknown)
a1.href = "#"
a1.onclick = router.node(d.node)
td2.appendChild(a1)
if (!unknown && has_location(d.node)) { if (!unknown && has_location(d.node)) {
var span = document.createElement("span") var span = document.createElement("span")

View file

@ -4,12 +4,14 @@ function (moment, Router, L, GUI, numeral) {
function handleData(data) { function handleData(data) {
var dataNodes = {} var dataNodes = {}
dataNodes.nodes = [] dataNodes.nodes = []
dataNodes.nodeIds = []
var dataGraph = {} var dataGraph = {}
dataGraph.batadv = {} dataGraph.batadv = {}
dataGraph.batadv.nodes = [] dataGraph.batadv.nodes = []
dataGraph.batadv.links = [] dataGraph.batadv.links = []
function rearrangeLinks(d) { function rearrangeLinks(d) {
d.source += dataGraph.batadv.nodes.length d.source += dataGraph.batadv.nodes.length
d.target += dataGraph.batadv.nodes.length d.target += dataGraph.batadv.nodes.length
@ -32,11 +34,22 @@ function (moment, Router, L, GUI, numeral) {
vererr = "Unsupported nodes version: " + data[i].version vererr = "Unsupported nodes version: " + data[i].version
console.log(vererr) //silent fail console.log(vererr) //silent fail
} else { } else {
dataNodes.nodes = dataNodes.nodes.concat(data[i].nodes) data[i].nodes.forEach(fillData)
dataNodes.timestamp = data[i].timestamp dataNodes.timestamp = data[i].timestamp
} }
} }
function fillData (node) {
var position = dataNodes.nodeIds.indexOf(node.nodeinfo.node_id)
if(position === -1){
dataNodes.nodes.push(node)
dataNodes.nodeIds.push(node.nodeinfo.node_id)
}
else
if(node.flags.online === true)
dataNodes.nodes[position] = node
}
var nodes = dataNodes.nodes.filter( function (d) { var nodes = dataNodes.nodes.filter( function (d) {
return "firstseen" in d && "lastseen" in d return "firstseen" in d && "lastseen" in d
}) })
@ -110,17 +123,44 @@ function (moment, Router, L, GUI, numeral) {
nodes.forEach( function (d) { nodes.forEach( function (d) {
d.neighbours = [] d.neighbours = []
if (d.statistics) {
/*eslint camelcase:0*/
if ("gateway" in d.statistics && d.statistics.gateway in graphnodes)
d.statistics.gateway = {"node": graphnodes[d.statistics.gateway], "id": d.statistics.gateway}
if ("nexthop" in d.statistics && d.statistics.nexthop in graphnodes)
d.statistics.nexthop = {"node": graphnodes[d.statistics.nexthop], "id": d.statistics.nexthop}
if ("gateway_nexthop" in d.statistics && d.statistics.gateway_nexthop in graphnodes)
d.statistics.gateway_nexthop = {"node": graphnodes[d.statistics.gateway_nexthop], "id": d.statistics.gateway_nexthop}
}
}) })
links.forEach( function (d) { links.forEach( function (d) {
if (d.type === "tunnel") if (d.type === "tunnel" || d.vpn) {
d.type = "VPN" d.type = "VPN"
else if (d.type === "wireless") d.isVPN = true
} else if (d.type === "fastd") {
d.type = "fastd"
d.isVPN = true
} else if (d.type === "l2tp") {
d.type = "L2TP"
d.isVPN = true
} else if (d.type === "gre") {
d.type = "GRE"
d.isVPN = true
} else if (d.type === "wireless") {
d.type = "Wifi" d.type = "Wifi"
else if (d.type === "other") d.isVPN = false
} else if (d.type === "other") {
d.type = "Kabel" d.type = "Kabel"
else d.isVPN = false
} else {
d.type = "N/A" d.type = "N/A"
d.isVPN = false
}
if (d.isVPN && d.target.node)
d.target.node.flags.uplink = true
var unknown = (d.source.node === undefined) var unknown = (d.source.node === undefined)
if (unknown) { if (unknown) {
d.target.node.neighbours.push({ id: d.source.id, link: d, incoming: true }) d.target.node.neighbours.push({ id: d.source.id, link: d, incoming: true })
@ -128,7 +168,7 @@ function (moment, Router, L, GUI, numeral) {
} }
d.source.node.neighbours.push({ node: d.target.node, link: d, incoming: false }) d.source.node.neighbours.push({ node: d.target.node, link: d, incoming: false })
d.target.node.neighbours.push({ node: d.source.node, link: d, incoming: true }) d.target.node.neighbours.push({ node: d.source.node, link: d, incoming: true })
if (d.type !== "VPN") if (!d.isVPN)
d.source.node.meshlinks = d.source.node.meshlinks ? d.source.node.meshlinks + 1 : 1 d.source.node.meshlinks = d.source.node.meshlinks ? d.source.node.meshlinks + 1 : 1
}) })

View file

@ -18,7 +18,7 @@ define(["map/clientlayer", "map/labelslayer",
onAdd: function () { onAdd: function () {
var button = L.DomUtil.create("button", "add-layer") var button = L.DomUtil.create("button", "add-layer")
button.textContent = "" button.textContent = "\uF2C7"
// L.DomEvent.disableClickPropagation(button) // L.DomEvent.disableClickPropagation(button)
// Click propagation isn't disabled as this causes problems with the // Click propagation isn't disabled as this causes problems with the
@ -46,7 +46,7 @@ define(["map/clientlayer", "map/labelslayer",
onAdd: function () { onAdd: function () {
var button = L.DomUtil.create("button", "locate-user") var button = L.DomUtil.create("button", "locate-user")
button.textContent = "" button.textContent = "\uF2E9"
L.DomEvent.disableClickPropagation(button) L.DomEvent.disableClickPropagation(button)
L.DomEvent.addListener(button, "click", this.onClick, this) L.DomEvent.addListener(button, "click", this.onClick, this)
@ -85,7 +85,7 @@ define(["map/clientlayer", "map/labelslayer",
onAdd: function () { onAdd: function () {
var button = L.DomUtil.create("button", "coord-picker") var button = L.DomUtil.create("button", "coord-picker")
button.textContent = "" button.textContent = "\uF2A6"
// Click propagation isn't disabled as this causes problems with the // Click propagation isn't disabled as this causes problems with the
// location picking mode; instead propagation is stopped in onClick(). // location picking mode; instead propagation is stopped in onClick().
@ -172,7 +172,7 @@ define(["map/clientlayer", "map/labelslayer",
var map, userLocation var map, userLocation
var layerControl var layerControl
var customLayers = new Set() var customLayers = {}
var baseLayers = {} var baseLayers = {}
var locateUserButton = new LocateButton(function (d) { var locateUserButton = new LocateButton(function (d) {
@ -259,16 +259,16 @@ define(["map/clientlayer", "map/labelslayer",
if (layerName in baseLayers) if (layerName in baseLayers)
return return
if (customLayers.has(layerName)) if (layerName in customLayers)
return return
try { try {
var layer = L.tileLayer.provider(layerName) var layer = L.tileLayer.provider(layerName)
layerControl.addBaseLayer(layer, layerName) layerControl.addBaseLayer(layer, layerName)
customLayers.add(layerName) customLayers[layerName] = layer
if (localStorageTest()) if (localStorageTest())
localStorage.setItem("map/customLayers", JSON.stringify(Array.from(customLayers))) localStorage.setItem("map/customLayers", JSON.stringify(Object.keys(customLayers)))
} catch (e) { } catch (e) {
return return
} }
@ -286,16 +286,10 @@ define(["map/clientlayer", "map/labelslayer",
var layers = config.mapLayers.map( function (d) { var layers = config.mapLayers.map( function (d) {
return { return {
"name": d.name, "name": d.name,
"layer": "url" in d ? L.tileLayer(d.url, d.config) : L.tileLayer.provider(d.name) "layer": "url" in d ? "layers" in d.config ? L.tileLayer.wms(d.url, d.config) : L.tileLayer(d.url, d.config) : L.tileLayer.provider(d.name)
} }
}) })
var maxLayerZoom = Math.max.apply(Math, config.mapLayers.map(
function(d) {
return (typeof d.config !== "undefined" && typeof d.config.maxZoom !== "undefined") ? d.config.maxZoom : 18
}))
layers[0].layer.addTo(map) layers[0].layer.addTo(map)
layers.forEach( function (d) { layers.forEach( function (d) {
@ -324,16 +318,33 @@ define(["map/clientlayer", "map/labelslayer",
if (d) if (d)
d.forEach(addLayer) d.forEach(addLayer)
d = JSON.parse(localStorage.getItem("map/selectedLayer"))
d = d && d.name in baseLayers ? baseLayers[d.name] : d && d.name in customLayers ? customLayers[d.name] : false
if (d) {
map.removeLayer(layers[0].layer)
map.addLayer(d)
}
} }
var clientLayer = new ClientLayer({minZoom: 15, maxZoom: maxLayerZoom}) var clientLayer = new ClientLayer({minZoom: 15})
clientLayer.addTo(map) clientLayer.addTo(map)
clientLayer.setZIndex(5) clientLayer.setZIndex(5)
var labelsLayer = new LabelsLayer({maxZoom: maxLayerZoom}) var labelsLayer = new LabelsLayer({})
labelsLayer.addTo(map) labelsLayer.addTo(map)
labelsLayer.setZIndex(6) labelsLayer.setZIndex(6)
map.on("baselayerchange", function(e) {
map.options.maxZoom = e.layer.options.maxZoom
clientLayer.options.maxZoom = map.options.maxZoom
labelsLayer.options.maxZoom = map.options.maxZoom
if (map.getZoom() > map.options.maxZoom) map.setZoom(map.options.maxZoom)
if (localStorageTest())
localStorage.setItem("map/selectedLayer", JSON.stringify({name: e.name}))
})
var nodeDict = {} var nodeDict = {}
var linkDict = {} var linkDict = {}
var highlight var highlight

View file

@ -1,15 +1,12 @@
define(["leaflet", "jshashes"], define(["leaflet"],
function (L, jsHashes) { function (L) {
var MD5 = new jsHashes.MD5()
return L.TileLayer.Canvas.extend({ return L.TileLayer.Canvas.extend({
setData: function (d) { setData: function (d) {
this.data = d this.data = d
//pre-calculate start angles //pre-calculate start angles
this.data.all().forEach(function (d) { this.data.all().forEach(function (d) {
var hash = MD5.hex(d.node.nodeinfo.node_id) d.startAngle = (parseInt(d.node.nodeinfo.node_id.substr(10, 2), 16) / 255) * 2 * Math.PI
d.startAngle = (parseInt(hash.substr(0, 2), 16) / 255) * 2 * Math.PI
}) })
this.redraw() this.redraw()
}, },

View file

@ -6,16 +6,22 @@ define(function () {
self.setData = function (d) { self.setData = function (d) {
var totalNodes = sum(d.nodes.all.map(one)) var totalNodes = sum(d.nodes.all.map(one))
var totalOnlineNodes = sum(d.nodes.all.filter(online).map(one)) var totalOnlineNodes = sum(d.nodes.all.filter(online).map(one))
var totalOfflineNodes = sum(d.nodes.all.filter(function (node) {return !node.flags.online}).map(one))
var totalNewNodes = sum(d.nodes.new.map(one)) var totalNewNodes = sum(d.nodes.new.map(one))
var totalLostNodes = sum(d.nodes.lost.map(one)) var totalLostNodes = sum(d.nodes.lost.map(one))
var totalClients = sum(d.nodes.all.filter(online).map( function (d) { var totalClients = sum(d.nodes.all.filter(online).map( function (d) {
return d.statistics.clients return d.statistics.clients ? d.statistics.clients : 0
})) }))
var totalGateways = sum(d.nodes.all.filter(online).filter( function (d) { var totalGateways = sum(Array.from(new Set(d.nodes.all.filter(online).map( function(d) {
return ("gateway" in d.statistics && d.statistics.gateway.id) ? d.statistics.gateway.id : d.statistics.gateway
}).concat(d.nodes.all.filter( function (d) {
return d.flags.gateway return d.flags.gateway
}).map(one)) })))).map(function(d) {
return (typeof d === "string") ? 1 : 0
}))
var nodetext = [{ count: totalOnlineNodes, label: "online" }, var nodetext = [{ count: totalOnlineNodes, label: "online" },
{ count: totalOfflineNodes, label: "offline" },
{ count: totalNewNodes, label: "neu" }, { count: totalNewNodes, label: "neu" },
{ count: totalLostNodes, label: "verschwunden" } { count: totalLostNodes, label: "verschwunden" }
].filter( function (d) { return d.count > 0 } ) ].filter( function (d) { return d.count > 0 } )

View file

@ -21,7 +21,11 @@ define(["sorttable", "virtual-dom", "numeral"], function (SortTable, V, numeral)
var headings = [{ name: "Knoten", var headings = [{ name: "Knoten",
sort: function (a, b) { sort: function (a, b) {
return a.nodeinfo.hostname.localeCompare(b.nodeinfo.hostname) var aname = typeof a.nodeinfo.hostname === "string" ? a.nodeinfo.hostname : a.nodeinfo.node_id
var bname = typeof b.nodeinfo.hostname === "string" ? b.nodeinfo.hostname : b.nodeinfo.node_id
if (typeof aname === "string" && typeof bname === "string")
return aname.localeCompare(bname)
return typeof aname === "string" ? 1 : typeof bname === "string" ? -1 : 0
}, },
reverse: false reverse: false
}, },
@ -52,7 +56,7 @@ define(["sorttable", "virtual-dom", "numeral"], function (SortTable, V, numeral)
td1Content.push(V.h("a", { className: aClass.join(" "), td1Content.push(V.h("a", { className: aClass.join(" "),
onclick: router.node(d), onclick: router.node(d),
href: "#" href: "#!n:" + d.nodeinfo.node_id
}, d.nodeinfo.hostname)) }, d.nodeinfo.hostname))
if (has_location(d)) if (has_location(d))

View file

@ -128,7 +128,13 @@ define(["chroma-js", "virtual-dom", "numeral-intl", "filters/genericnode", "verc
return d ? "online" : "offline" return d ? "online" : "offline"
}) })
var fwDict = count(nodes, ["nodeinfo", "software", "firmware", "release"]) var fwDict = count(nodes, ["nodeinfo", "software", "firmware", "release"])
var hwDict = count(nodes, ["nodeinfo", "hardware", "model"]) var hwDict = count(nodes, ["nodeinfo", "hardware", "model"], function (d) {
if (d) {
d = d.replace(/\(r\)|\(tm\)/gi, "").replace(/AMD |Intel |TP-Link | CPU| Processor/g, "")
if (d.indexOf("@") > 0) d = d.substring(0, d.indexOf("@"))
}
return d
})
var geoDict = count(nodes, ["nodeinfo", "location"], function (d) { var geoDict = count(nodes, ["nodeinfo", "location"], function (d) {
return d && d.longitude && d.latitude ? "ja" : "nein" return d && d.longitude && d.latitude ? "ja" : "nein"
}) })
@ -146,12 +152,15 @@ define(["chroma-js", "virtual-dom", "numeral-intl", "filters/genericnode", "verc
return d ? "ja" : "nein" return d ? "ja" : "nein"
}) })
var gwNodesDict = count(nodes, ["statistics", "gateway"], function (d) { var gwNodesDict = count(onlineNodes, ["statistics", "gateway"], function (d) {
if (d === null) if (d === null)
return null return null
if (d in nodeDict) if (d.node)
return nodeDict[d].nodeinfo.hostname return d.node.nodeinfo.hostname
if (d.id)
return d.id
return d return d
}) })
@ -160,13 +169,16 @@ define(["chroma-js", "virtual-dom", "numeral-intl", "filters/genericnode", "verc
if (d === null) if (d === null)
return null return null
if (d in nodeDict) if (d.node)
return nodeDict[d].nodeinfo.hostname return d.node.nodeinfo.hostname
if (d.id)
return d.id
return d return d
}) })
var siteDict = count(onlineNodes, ["nodeinfo", "system", "site_code"], function (d) { var siteDict = count(nodes, ["nodeinfo", "system", "site_code"], function (d) {
var rt = d var rt = d
if (config.siteNames) if (config.siteNames)
config.siteNames.forEach( function (t) { config.siteNames.forEach( function (t) {
@ -182,8 +194,8 @@ define(["chroma-js", "virtual-dom", "numeral-intl", "filters/genericnode", "verc
fillTable("Koordinaten", geoTable, geoDict.sort(function (a, b) { return b[1] - a[1] })) fillTable("Koordinaten", geoTable, geoDict.sort(function (a, b) { return b[1] - a[1] }))
fillTable("Uplink", uplinkTable, uplinkDict.sort(function (a, b) { return b[1] - a[1] })) fillTable("Uplink", uplinkTable, uplinkDict.sort(function (a, b) { return b[1] - a[1] }))
fillTable("Autom. Updates", autoTable, autoDict.sort(function (a, b) { return b[1] - a[1] })) fillTable("Autom. Updates", autoTable, autoDict.sort(function (a, b) { return b[1] - a[1] }))
fillTable("Nodes an Gateway", gwNodesTable, gwNodesDict.sort(function (a, b) { return b[1] - a[1] })) fillTable("Gateway", gwNodesTable, gwNodesDict.sort(function (a, b) { return b[1] - a[1] }))
fillTable("Clients an Gateway", gwClientsTable, gwClientsDict.sort(function (a, b) { return b[1] - a[1] })) fillTable("Gateway", gwClientsTable, gwClientsDict.sort(function (a, b) { return b[1] - a[1] }))
fillTable("Site", siteTable, siteDict.sort(function (a, b) { return b[1] - a[1] })) fillTable("Site", siteTable, siteDict.sort(function (a, b) { return b[1] - a[1] }))
} }

View file

@ -78,6 +78,8 @@ define(function () {
if (!s) if (!s)
return false return false
s = decodeURIComponent(s)
if (!s.startsWith("#!")) if (!s.startsWith("#!"))
return false return false
@ -94,7 +96,7 @@ define(function () {
var id var id
if (args[0] === "n") { if (args[0] === "n") {
id = decodeURIComponent(args[1]) id = args[1]
if (id in objects.nodes) { if (id in objects.nodes) {
currentObject = { node: objects.nodes[id] } currentObject = { node: objects.nodes[id] }
gotoNode(objects.nodes[id]) gotoNode(objects.nodes[id])
@ -103,7 +105,7 @@ define(function () {
} }
if (args[0] === "l") { if (args[0] === "l") {
id = decodeURIComponent(args[1]) id = args[1]
if (id in objects.links) { if (id in objects.links) {
currentObject = { link: objects.links[id] } currentObject = { link: objects.links[id] }
gotoLink(objects.links[id]) gotoLink(objects.links[id])

View file

@ -41,7 +41,7 @@ define(["moment", "virtual-dom"], function (moment, V) {
td1Content.push(V.h("a", { className: aClass.join(" "), td1Content.push(V.h("a", { className: aClass.join(" "),
onclick: router.node(d), onclick: router.node(d),
href: "#" href: "#!n:" + d.nodeinfo.node_id
}, d.nodeinfo.hostname)) }, d.nodeinfo.hostname))
if (has_location(d)) if (has_location(d))

View file

@ -1,22 +1,41 @@
{ {
"name": "hopglass", "name": "hopglass",
"version": "1.0.0",
"scripts": { "scripts": {
"test": "node -e \"require('grunt').cli()\" '' clean lint" "test": "node -e \"require('grunt').cli()\" '' clean lint"
}, },
"devDependencies": { "devDependencies": {
"grunt": "^0.4.5", "autoprefixer": "^6.3.3",
"dart-sass": "^1.16.1",
"grunt": "^1.0.3",
"grunt-check-dependencies": "^0.6.0", "grunt-check-dependencies": "^0.6.0",
"grunt-contrib-clean": "^0.6.0", "grunt-contrib-clean": "^0.6.0",
"grunt-contrib-connect": "^0.8.0", "grunt-contrib-connect": "^0.8.0",
"grunt-contrib-copy": "^0.5.0", "grunt-contrib-copy": "^0.5.0",
"grunt-contrib-cssmin": "^0.12.2", "grunt-contrib-cssmin": "^0.12.2",
"grunt-contrib-requirejs": "^0.4.4", "grunt-contrib-requirejs": "^0.4.4",
"grunt-contrib-sass": "^0.9.2",
"grunt-contrib-uglify": "^0.5.1", "grunt-contrib-uglify": "^0.5.1",
"grunt-contrib-watch": "^0.6.1", "grunt-contrib-watch": "^0.6.1",
"grunt-eslint": "^10.0.0", "grunt-eslint": "^10.0.0",
"grunt-bower-install-simple": "^1.1.2", "grunt-git-describe": "^2.3.2",
"grunt-git-describe": "^2.3.2" "grunt-postcss": "^0.7.2",
"grunt-sass": "^3.0.2"
},
"dependencies": {
"almond": "^0.3.3",
"chroma-js": "^0.7.8",
"d3": "^3.5.17",
"ionicons": "^2.0.1",
"leaflet": "^0.7.7",
"leaflet-label": "^0.2.1-0",
"leaflet-providers": "^1.5.0",
"moment": "^2.23.0",
"numeraljs": "^1.5.6",
"rbush": "^1.4.3",
"requirejs": "^2.3.2",
"roboto-fontface": "^0.10.0",
"tablesort": "3.0.2",
"virtual-dom": "^2.1.1"
}, },
"eslintConfig": { "eslintConfig": {
"env": { "env": {

View file

@ -28,3 +28,7 @@ h5 {
h6 { h6 {
font-size: 0.67em; font-size: 0.67em;
} }
.hide {
display: none;
}

View file

@ -1,28 +0,0 @@
.infobox .chart {
position: relative;
& > .c3 {
margin-right: 20px;
}
.zoom-buttons {
position: absolute;
top: 0px;
right: 20px;
button {
font-size: 10pt;
width: 3em;
height: 3em;
border-radius: 1.5em;
margin-left: 6px;
}
}
.c3-tooltip-container {
width: 150px;
.c3-tooltip {
border-collapse: collapse;
}
}
}

View file

@ -2,7 +2,7 @@
margin: 0; margin: 0;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
font-family: Roboto; font-family: Roboto, sans-serif;
font-size: 0.83em; font-size: 0.83em;
font-weight: bold; font-weight: bold;
padding: 0 6pt 6pt !important; padding: 0 6pt 6pt !important;
@ -10,7 +10,7 @@
li { li {
border-radius: 20pt; border-radius: 20pt;
display: flex; display: flex;
padding: 0pt 0 0pt 8pt; padding: 0 0 0 8pt;
margin: 3pt; margin: 3pt;
align-items: center; align-items: center;
background: #009ee0; background: #009ee0;

View file

@ -1 +1 @@
../bower_components/Leaflet.label/dist/leaflet.label.css ../node_modules/leaflet-label/dist/leaflet.label.css

View file

@ -1 +1 @@
../bower_components/leaflet/dist/leaflet.css ../node_modules/leaflet/dist/leaflet.css

View file

@ -1,5 +1,4 @@
.legend .symbol .legend .symbol {
{
width: 1em; width: 1em;
height: 1em; height: 1em;
border-radius: 50%; border-radius: 50%;
@ -7,22 +6,18 @@
vertical-align: -5%; vertical-align: -5%;
} }
.legend-new .symbol .legend-new .symbol {
{
background-color: #93E929; background-color: #93E929;
} }
.legend-online .symbol .legend-online .symbol {
{
background-color: #1566A9; background-color: #1566A9;
} }
.legend-offline .symbol .legend-offline .symbol {
{
background-color: #D43E2A; background-color: #D43E2A;
} }
.legend-online, .legend-offline .legend-online, .legend-offline {
{
margin-left: 1em; margin-left: 1em;
} }

23
scss/_loader.scss Normal file
View file

@ -0,0 +1,23 @@
.loader {
color: #000000;
font-size: 1.8em;
line-height: 2;
margin: 30vh auto;
text-align: center;
}
.spinner {
animation: .6s spinner ease-in-out infinite alternate;
border-bottom: 2px solid #000000;
border-radius: 50%;
display: inline-block;
height: 64px;
margin-top: 10px;
width: 64px;
}
@keyframes spinner {
to {
transform: rotate(360deg);
}
}

View file

@ -35,13 +35,25 @@
} }
@-webkit-keyframes blink { @-webkit-keyframes blink {
0% { opacity: 1.0; } 0% {
80% { opacity: 1.0; } opacity: 1.0;
90% { opacity: 0.0; } }
80% {
opacity: 1.0;
}
90% {
opacity: 0.0;
}
} }
@keyframes blink { @keyframes blink {
0% { opacity: 1.0; } 0% {
80% { opacity: 1.0; } opacity: 1.0;
90% { opacity: 0.0; } }
80% {
opacity: 1.0;
}
90% {
opacity: 0.0;
}
} }

View file

@ -22,25 +22,31 @@ time, mark, audio, video {
font: inherit; font: inherit;
vertical-align: baseline; vertical-align: baseline;
} }
/* HTML5 display-role reset for older browsers */ /* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure, article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section { footer, header, hgroup, menu, nav, section {
display: block; display: block;
} }
body { body {
line-height: 1; line-height: 1;
} }
ol, ul { ol, ul {
list-style: none; list-style: none;
} }
blockquote, q { blockquote, q {
quotes: none; quotes: none;
} }
blockquote:before, blockquote:after, blockquote:before, blockquote:after,
q:before, q:after { q:before, q:after {
content: ''; content: '';
content: none; content: none;
} }
table { table {
border-collapse: collapse; border-collapse: collapse;
border-spacing: 0; border-spacing: 0;

View file

@ -3,17 +3,13 @@
@mixin shadow($level:1) { @mixin shadow($level:1) {
@if $level == 1 { @if $level == 1 {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
} } @else if $level == 2 {
@else if $level == 2 {
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23); box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
} } @else if $level == 3 {
@else if $level == 3 {
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23); box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23);
} } @else if $level == 4 {
@else if $level == 4 {
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
} } @else if $level == 5 {
@else if $level == 5 {
box-shadow: 0 19px 38px rgba(0, 0, 0, 0.30), 0 15px 12px rgba(0, 0, 0, 0.22); box-shadow: 0 19px 38px rgba(0, 0, 0, 0.30), 0 15px 12px rgba(0, 0, 0, 0.22);
} }
} }

View file

@ -0,0 +1,36 @@
.nodeheader {
width: 90%;
position: relative;
z-index: 2;
}
.nodeheader img {
width: 30%;
max-height: 180px;
margin: auto;
text-align: center;
max-width: 128px;
display: block;
z-index: 2;
}
.nodeheader img .none {
display: none;
}
.nodeheader span {
background-color: silver;
background-color: hsla(0, 0%, 100%, 0.5);
position: absolute;
bottom: 0;
width: 100%;
line-height: 1.5em;
text-align: center;
color: black;
font-weight: bold;
font-size: large;
z-index: 2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

View file

@ -4,6 +4,7 @@
@import '_leaflet'; @import '_leaflet';
@import '_leaflet.label'; @import '_leaflet.label';
@import '_filters'; @import '_filters';
@import '_loader';
$minscreenwidth: 630pt; $minscreenwidth: 630pt;
$sidebarwidth: 420pt; $sidebarwidth: 420pt;
@ -14,7 +15,6 @@ $buttondistance: 12pt;
@import '_map'; @import '_map';
@import '_forcegraph'; @import '_forcegraph';
@import '_legend'; @import '_legend';
@import '_chart';
.content { .content {
position: fixed; position: fixed;
@ -45,7 +45,7 @@ $buttondistance: 12pt;
margin: 0; margin: 0;
list-style: none; list-style: none;
display: flex; display: flex;
font-family: Roboto; font-family: Roboto, sans-serif;
@include shadow(1); @include shadow(1);
} }
@ -70,8 +70,9 @@ $buttondistance: 12pt;
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
font-family: 'Roboto Slab', serif; font-family: 'Roboto-Slab', serif;
font-size: 11pt; font-size: 11pt;
color: #333;
} }
th.sort-header::selection { th.sort-header::selection {
@ -117,6 +118,38 @@ table.attributes td {
line-height: 1.41em; line-height: 1.41em;
} }
table.attributes tr.routerpic {
max-height: 128px;
max-width: 128px;
min-width: 128px;
min-height: 128px;
/*height: 128px;*/
/*background-color: green;*/
}
table.attributes tr.routerpic td {
font-weight: bold;
/*background-color: red;*/
font-size: large;
vertical-align:bottom;
}
table.attributes tr.routerpic th {
font-weight: bold;
/*background-color: red;*/
font-size: large;
vertical-align:bottom;
}
.nodenamesidebar {
position: relative;
font-weight: bold;
/*background-color: red;*/
font-size: large;
vertical-align:bottom;
bottom: 0px;
}
.sidebar { .sidebar {
.infobox, .container { .infobox, .container {
@include shadow(2); @include shadow(2);
@ -144,6 +177,13 @@ table.attributes td {
white-space: normal; white-space: normal;
} }
.infobox .clientsMesh {
font-family: "ionicons";
color: #dbdbdb;
word-spacing: -0.2em;
white-space: normal;
}
.infobox { .infobox {
position: relative; position: relative;
padding: 0.25em 0; padding: 0.25em 0;
@ -152,6 +192,27 @@ table.attributes td {
img { img {
max-width: 100%; max-width: 100%;
} }
input[type="text"], textarea {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
border-radius: 5px;
border: none;
line-height: 1.67em;
vertical-align: bottom;
margin-right: 0.7em;
padding: 3px 6px;
font-family: monospace;
width: 70%;
max-width: 500px;
min-height: 42px;
font-size: 1.15em;
}
textarea {
resize: vertical;
overflow: auto;
max-height: 300px;
}
} }
button { button {
@ -160,6 +221,7 @@ button {
@include shadow(1); @include shadow(1);
border-radius: 0.9em; border-radius: 0.9em;
background: rgba(255, 255, 255, 0.7); background: rgba(255, 255, 255, 0.7);
color: #333;
border: none; border: none;
cursor: pointer; cursor: pointer;
height: 1.8em; height: 1.8em;
@ -180,7 +242,7 @@ button:hover {
} }
button:active { button:active {
box-shadow: inset 0px 5px 20px rgba(0, 0, 0, 0.19), inset 0px 3px 6px rgba(0, 0, 0, 0.23); box-shadow: inset 0 5px 20px rgba(0, 0, 0, 0.19), inset 0 3px 6px rgba(0, 0, 0, 0.23);
} }
button::-moz-focus-inner { button::-moz-focus-inner {
@ -199,6 +261,8 @@ button.close {
border-radius: 0; border-radius: 0;
color: rgba(0, 0, 0, 0.5); color: rgba(0, 0, 0, 0.5);
font-family: "ionicons"; font-family: "ionicons";
position: absolute;
right: 0;
&:hover { &:hover {
color: #dc0067; color: #dc0067;
@ -300,7 +364,7 @@ table {
position: absolute; position: absolute;
top: $buttondistance; top: $buttondistance;
left: $buttondistance; left: $buttondistance;
margin-bottom: $buttondistance; padding-bottom: $buttondistance;
transition: left 0.5s; transition: left 0.5s;
} }
@ -385,9 +449,9 @@ table {
@media screen and (max-width: 80em) { @media screen and (max-width: 80em) {
.sidebar { .sidebar {
font-size: 0.8em; font-size: 0.8em;
top: 0pt; top: 0;
left: 0pt; left: 0;
margin: 0pt; margin: 0;
width: $sidebarwidthsmall; width: $sidebarwidthsmall;
min-height: 100vh; min-height: 100vh;
@include shadow(2); @include shadow(2);
@ -418,7 +482,7 @@ table {
.sidebar { .sidebar {
position: static; position: static;
margin: 0em !important; margin: 0 !important;
width: auto; width: auto;
height: auto; height: auto;
min-height: 0; min-height: 0;

View file

@ -1,6 +1,6 @@
module.exports = function(grunt) { module.exports = function(grunt) {
grunt.config.merge({ grunt.config.merge({
bowerdir: "bower_components", nodedir: "node_modules",
copy: { copy: {
html: { html: {
options: { options: {
@ -19,26 +19,21 @@ module.exports = function(grunt) {
dest: "build/" dest: "build/"
}, },
vendorjs: { vendorjs: {
src: [ "es6-shim/es6-shim.min.js" ], src: ["es6-shim/es6-shim.min.js",
"es6-shim/es6-shim.map"],
expand: true, expand: true,
cwd: "bower_components/", cwd: "node_modules/",
dest: "build/vendor/" dest: "build/vendor/"
}, },
robotoSlab: {
src: [ "fonts/*",
"roboto-slab-fontface.css"
],
expand: true,
dest: "build/",
cwd: "bower_components/roboto-slab-fontface"
},
roboto: { roboto: {
src: [ "fonts/*", src: [ "fonts/roboto/*",
"roboto-fontface.css" "fonts/roboto-slab/*",
"css/roboto/roboto-fontface.css",
"css/roboto-slab/roboto-slab-fontface.css"
], ],
expand: true, expand: true,
dest: "build/", dest: "build/",
cwd: "bower_components/roboto-fontface" cwd: "node_modules/roboto-fontface/"
}, },
ionicons: { ionicons: {
src: [ "fonts/*", src: [ "fonts/*",
@ -46,59 +41,55 @@ module.exports = function(grunt) {
], ],
expand: true, expand: true,
dest: "build/", dest: "build/",
cwd: "bower_components/ionicons/" cwd: "node_modules/ionicons/"
}, },
leafletImages: { leafletImages: {
src: [ "images/*" ], src: [ "images/*" ],
expand: true, expand: true,
dest: "build/", dest: "build/",
cwd: "bower_components/leaflet/dist/" cwd: "node_modules/leaflet/dist/"
},
c3: {
src: ["c3.min.css"],
expand: true,
dest: "build/",
cwd: "bower_components/c3/"
} }
}, },
sass: { sass: {
dist: {
options: { options: {
style: "compressed" sourceMap: true,
outputStyle: "compressed",
implementation: require("dart-sass")
}, },
dist: {
files: { files: {
"build/style.css": "scss/main.scss" "build/style.css": "scss/main.scss"
} }
} }
}, },
postcss: {
options: {
map: true,
processors: [
require("autoprefixer")({
browsers: ["last 2 versions"]
})
]
},
dist: {
src: "build/style.css"
}
},
cssmin: { cssmin: {
target: { target: {
files: { files: {
"build/style.css": [ "bower_components/leaflet/dist/leaflet.css", "build/style.css": [ "node_modules/leaflet/dist/leaflet.css",
"bower_components/Leaflet.label/dist/leaflet.label.css", "node_modules/leaflet-label/dist/leaflet.label.css",
"style.css" "style.css"
] ]
} }
} }
}, },
"bower-install-simple": {
options: {
directory: "<%=bowerdir%>",
color: true,
interactive: false,
production: true
},
"prod": {
options: {
production: true
}
}
},
requirejs: { requirejs: {
compile: { compile: {
options: { options: {
baseUrl: "lib", baseUrl: "lib",
name: "../bower_components/almond/almond", name: "../node_modules/almond/almond",
mainConfigFile: "app.js", mainConfigFile: "app.js",
include: "../app", include: "../app",
wrap: true, wrap: true,
@ -109,8 +100,8 @@ module.exports = function(grunt) {
} }
}) })
grunt.loadNpmTasks("grunt-bower-install-simple")
grunt.loadNpmTasks("grunt-contrib-copy") grunt.loadNpmTasks("grunt-contrib-copy")
grunt.loadNpmTasks("grunt-contrib-requirejs") grunt.loadNpmTasks("grunt-contrib-requirejs")
grunt.loadNpmTasks("grunt-contrib-sass") grunt.loadNpmTasks("grunt-sass")
grunt.loadNpmTasks("grunt-postcss")
} }

View file

@ -4,11 +4,6 @@ module.exports = function (grunt) {
options: { options: {
install: true install: true
}, },
bower: {
options: {
packageManager: "bower"
}
},
npm: {} npm: {}
}, },
eslint: { eslint: {

3451
yarn.lock Normal file

File diff suppressed because it is too large Load diff