Compare commits

...

94 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
PetaByteBoy // Milan Pässler 5fce722f24 Merge pull request #12 from yayachiken/master
Fixed client-dots and labels not showing on zoom levels >= 19
2016-03-28 23:30:58 +02:00
Milan Pässler 08e00d4b91 infobox/node: nodeinfos {NODE_NAME} support 2016-03-28 22:58:45 +02:00
PetaByteBoy // Milan Pässler 1f07f50530 Merge pull request #13 from yayachiken/new-stats-patch
stats: Fix inaccuracies if lat or long is missing
2016-03-24 13:05:12 +01:00
David Kolossa 40dd7d2a0a stats: Fix inaccuracies if lat or long is missing
* please also note that freifunk-gluon/gluon#703 exists.
2016-03-24 00:46:42 +01:00
PetaByteBoy // Milan Pässler 54801ded8f Merge pull request #11 from eberhab/master
Added support for iframes in config
2016-03-23 13:42:27 +01:00
Benjamin 961a18fec4 infobox: added support for iframes 2016-03-23 13:41:02 +01:00
David Kolossa 78de449b88 map: fixed display errors on high zoom levels
* client dots and labels only showed up on zoom
  levels up to 18
2016-03-23 13:14:58 +01:00
PetaByteBoy // Milan Pässler f8701ca0e3 Merge pull request #9 from viisauksena/patch-1
patch: allow hiding of statistic elements and add cients/gw statistics
2016-03-22 17:42:59 +01:00
viisauksena fc2a1e3872 proportions: allow hiding of statistics and add gateway client statistics
Update proportions.js: enable onclick hiding of Statistics <H2> and add clients per GW <H2>
hiding works by clicking on H2 element, so you can have the overview you like
[need to add some hidden line in scss/main.scss also]
clients per GW is based on nodes.json and the offered clientcount by the nodes directly (so this is only rough idea of reality, for alfred this means extra timedifferences upto 30 minutes)

update main.scss: to allow hiding of statistics element
add this to allow statistics element hiding

use consistent var table and dict Names
change gwTable and gwDict (and gw2 ...) to
gwNodes and gwClients
2016-03-22 04:52:16 +01:00
PetaByteBoy // Milan Pässler d1e28192b1 Merge pull request #10 from ffggrz/master
added option to fix the map on a specific coordinate
2016-03-16 13:28:58 +01:00
ffggrz.de 8d22417789 added fixed center option
added option to fix the map on a specific coordinate
2016-03-16 00:39:09 +01:00
Milan Pässler 11a157c58a readme: add linkinfos 2016-03-08 20:07:20 +01:00
Milan Pässler 2eace51bfe add configurable linkinfos 2016-03-08 20:01:38 +01:00
Milan Pässler a7c18b3a0a infobox/node: eliminate duplicate 'vor' 2016-03-05 14:21:33 +01:00
Milan Pässler 2e414cd5b8 infobox/node: unseen status orange 2016-03-05 13:48:49 +01:00
Milan Pässler 8d4cc87994 main: unseen nodes are online 2016-03-05 13:48:49 +01:00
Milan Pässler f7bc41dcec infobox/node: lastseen proper quantity 2016-03-05 13:48:49 +01:00
Milan Pässler 309878d607 forcegraph: partially revert 'draw unseen nodes orange' 2016-03-05 13:48:44 +01:00
Milan Pässler 4c8b2c03c6 infobox/location: fix uci commands 2016-03-03 20:00:18 +01:00
Milan Pässler 593a96e07e infobox/node: more detailed lastseen 2016-03-02 22:34:35 +01:00
Milan Pässler 24987f4963 forcegraph: draw unseen nodes orange 2016-03-02 21:55:44 +01:00
Milan Pässler 23ded77089 infobox/location: more detailed command 2016-03-02 10:45:41 +01:00
Milan Pässler c541f5b721 main: fix missing location error 2016-03-01 19:50:14 +01:00
Milan Pässler 2e823a90c8 nodefilter: filter unknown nodes 2016-02-28 01:27:00 +01:00
Milan Pässler 97a00b6925 forcegraph: show links with unknown nodes 2016-02-27 14:43:47 +01:00
Milan Pässler 9f48cb1f3b location: remove debug prints
also some style corrections
2016-02-25 21:38:01 +01:00
Milan Pässler 273d0d5d23 infobox/location: show uci in one box 2016-02-25 21:15:28 +01:00
Milan Pässler 8758c04a29 infobox/node: always show lastseen
also change loadavg name
2016-02-25 21:00:05 +01:00
46 changed files with 4602 additions and 537 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.
@ -84,9 +85,25 @@ property and optionally `url` and `config` properties. If no `url` is supplied
`name` is assumed to name a `name` is assumed to name a
[Leaflet-provider](http://leaflet-extras.github.io/leaflet-providers/preview/). [Leaflet-provider](http://leaflet-extras.github.io/leaflet-providers/preview/).
## fixedCenter (array, optional)
This option allows to fix the map at one specific coordinate depending on following case-sensitive parameters:
- `lat` latitude of the center point
- `lng` longitude of the center point
- `radius` visible radius around the center in km
Examples for `fixedCenter`:
"fixedCenter": {
"lat": 50.80,
"lng": 12.07,
"radius": 30
}
## nodeInfos (array, optional) ## nodeInfos (array, optional)
This option allows to show client statistics depending on following case-sensitive parameters: This option allows to show node statistics depending on following case-sensitive parameters:
- `name` caption of statistics segment in infobox - `name` caption of statistics segment in infobox
- `href` absolute or relative URL to statistics image - `href` absolute or relative URL to statistics image
@ -103,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}"
} }
] ]
@ -137,6 +154,25 @@ Examples for `globalInfos` using Grafana server rendering:
} }
] ]
## linkInfos (array, optional)
This option allows to show link statistics depending on the following case-sensitive parameters:
- `name` caption of statistics segment in infobox
- `href` absolute or relative URL to statistics image
- `thumbnail` absolute or relative URL to thumbnail image,
can be the same like `href`
- `caption` is shown, if `thumbnail` is not present (no thumbnail in infobox)
To insert the source or target node-id in either `href`, `thumbnail` or `caption`
you can use the case-sensitive template strings `{SOURCE}` and `{TARGET}`.
"linkInfos": [
{ "href": "stats/dashboard/db/links?var-source={SOURCE}&var-target={TARGET}",
"thumbnail": "stats/render/dashboard-solo/db/links?panelId=1&fullscreen&theme=light&width=800&height=600&var-source={SOURCE}&var-target={TARGET}"
}
]
## siteNames (array, optional) ## siteNames (array, optional)
In this array name definitions for site statistics and node info can be saved. This requires one object for each site code. This object must contain: In this array name definitions for site statistics and node info can be saved. This requires one object for each site code. This object must contain:

26
app.js
View file

@ -1,20 +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"
}, },
shim: { shim: {
"leaflet.label": ["leaflet"], "leaflet.label": ["leaflet"],

View file

@ -1,36 +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"
},
"authors": [
"Milan Pässler <me@petabyteboy.de>",
"Nils Schneider <nils@nilsschneider.net>"
],
"license": "AGPL3",
"private": true
}

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,27 +1,62 @@
{ {
"dataPath": "https://map.luebeck.freifunk.net/data/", "dataPath": "https://map.ffdus.de/data/",
"siteName": "Freifunk Lübeck", "siteName": "Freifunk Flingern",
"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": "ffhl", "name": "Lübeck" }, { "site": "dus", "name": "Flingern" }
{ "site": "ffeh", "name": "Entenhausen" }, ],
{ "site": "ffgt", "name": "Gothamcity" }, "hwImg": [
{ "site": "ffal", "name": "Atlantis" } { "thumbnail": "https://cdn.rawgit.com/Moorviper/meshviewer_hwpics/master/nodes/{MODELHASH}.svg",
"caption": "Knoten {MODELHASH}"
}
] ]
} }

106
helper.js
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) {
@ -73,6 +73,14 @@ function localStorageTest() {
} }
} }
function listReplace(s, subst) {
for (key in subst) {
var re = new RegExp(key, 'g')
s = s.replace(re, subst[key])
}
return s
}
/* Helpers working with nodes */ /* Helpers working with nodes */
function offline(d) { function offline(d) {
@ -122,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")
@ -138,3 +152,77 @@ function attributeEntry(el, label, value) {
return td return td
} }
function createIframe(opt, width, height) {
el = document.createElement("iframe")
width = typeof width !== 'undefined' ? width : '100%'
height = typeof height !== 'undefined' ? height : '350px'
if (opt.src)
el.src = opt.src
else
el.src = opt
if (opt.frameBorder)
el.frameBorder = opt.frameBorder
else
el.frameBorder = 1
if (opt.width)
el.width = opt.width
else
el.width = width
if (opt.height)
el.height = opt.height
else
el.height = height
el.scrolling = "no"
el.seamless = "seamless"
return el
}
function showStat(o, subst) {
var content, caption
subst = typeof subst !== 'undefined' ? subst : {}
if (o.thumbnail) {
content = document.createElement("img")
content.src = listReplace(o.thumbnail, subst)
}
if (o.caption) {
caption = listReplace(o.caption, subst)
if (!content)
content = document.createTextNode(caption)
}
if (o.iframe) {
content = createIframe(o.iframe, o.width, o.height)
if (o.iframe.src)
content.src = listReplace(o.iframe.src, subst)
else
content.src = listReplace(o.iframe, subst)
}
var p = document.createElement("p")
if (o.href) {
var link = document.createElement("a")
link.target = "_blank"
link.href = listReplace(o.href, subst)
link.appendChild(content)
if (caption && o.thumbnail)
link.title = caption
p.appendChild(link)
} else
p.appendChild(content)
return p
}

View file

@ -4,8 +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="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>
@ -14,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,14 +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="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,18 +4,18 @@ 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()
n.graph = {} n.graph = {}
n.graph.nodes = data.graph.nodes.filter( function (d) { n.graph.nodes = data.graph.nodes.filter( function (d) {
if (!d.node) var r
return true if (d.node)
r = filter(d.node)
var r = filter(d.node) else
r = filter({})
if (r) if (r)
filteredIds.add(d.id) filteredIds.add(d.id)

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
@ -19,6 +19,7 @@ define(["d3"], function (d3) {
var nodes = [] var nodes = []
var uplinkNodes = [] var uplinkNodes = []
var nonUplinkNodes = [] var nonUplinkNodes = []
var unseenNodes = []
var unknownNodes = [] var unknownNodes = []
var savedPanZoom var savedPanZoom
@ -213,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) {
@ -241,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)
@ -313,6 +315,7 @@ define(["d3"], function (d3) {
var unknownColor = "#D10E2A" var unknownColor = "#D10E2A"
var nonUplinkColor = "#F2E3C6" var nonUplinkColor = "#F2E3C6"
var uplinkColor = "#5BAAEB" var uplinkColor = "#5BAAEB"
var unseenColor = "#FFA726"
var highlightColor = "rgba(252, 227, 198, 0.15)" var highlightColor = "rgba(252, 227, 198, 0.15)"
var nodeRadius = 6 var nodeRadius = 6
var cableColor = "#50B0F0" var cableColor = "#50B0F0"
@ -322,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()
}) })
@ -364,11 +370,17 @@ define(["d3"], function (d3) {
ctx.drawImage(uplinkNode, scale * r * d.x - uplinkNode.width / 2, scale * r * d.y - uplinkNode.height / 2) ctx.drawImage(uplinkNode, scale * r * d.x - uplinkNode.width / 2, scale * r * d.y - uplinkNode.height / 2)
}) })
var unseenNode = drawNode(unseenColor, nodeRadius, scale, r)
unseenNodes.filter(visibleNodes).forEach(function (d) {
ctx.drawImage(unseenNode, scale * r * d.x - unseenNode.width / 2, scale * r * d.y - unseenNode.height / 2)
})
ctx.restore() ctx.restore()
// -- 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)
@ -463,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) {
@ -516,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
}) })
@ -577,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)
@ -637,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()
@ -685,19 +699,29 @@ define(["d3"], function (d3) {
d.source.neighbours[d.target.o.id] = {node: d.target, link: d} d.source.neighbours[d.target.o.id] = {node: d.target, link: d}
d.target.neighbours[d.source.o.id] = {node: d.source, link: d} d.target.neighbours[d.source.o.id] = {node: d.source, link: d}
if (d.o.source.node && d.o.target.node) if (d.o.source && d.o.target)
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]
}) })
}) })
nodes = intNodes.filter(function (d) { return d.o.node }) nodes = intNodes.filter(function (d) { return !d.o.unseen && d.o.node })
uplinkNodes = nodes.filter(function (d) { return d.o.node.flags.uplink }) uplinkNodes = nodes.filter(function (d) { return d.o.node.flags.uplink })
nonUplinkNodes = nodes.filter(function (d) { return !d.o.node.flags.uplink }) nonUplinkNodes = nodes.filter(function (d) { return !d.o.node.flags.uplink })
unseenNodes = intNodes.filter(function (d) { return d.o.unseen && d.o.node })
unknownNodes = intNodes.filter(function (d) { return !d.o.node }) unknownNodes = intNodes.filter(function (d) { return !d.o.node })
if (localStorageTest()) { if (localStorageTest()) {

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,10 +1,22 @@
define(function () { define(function () {
function showStatImg(o, d) {
var subst = {}
subst["{SOURCE}"] = d.source.node_id
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 function (config, el, router, d) { return function (config, el, router, d) {
var unknown = !(d.source.node)
var h2 = document.createElement("h2") var h2 = document.createElement("h2")
var a1 = document.createElement("a") var a1 = document.createElement("a")
if (!unknown) {
a1.href = "#" a1.href = "#"
a1.onclick = router.node(d.source.node) a1.onclick = router.node(d.source.node)
a1.textContent = d.source.node.nodeinfo.hostname }
a1.textContent = unknown ? d.source.id : d.source.node.nodeinfo.hostname
h2.appendChild(a1) h2.appendChild(a1)
h2.appendChild(document.createTextNode(" → ")) h2.appendChild(document.createTextNode(" → "))
var a2 = document.createElement("a") var a2 = document.createElement("a")
@ -20,10 +32,17 @@ define(function () {
attributeEntry(attributes, "TQ", showTq(d)) attributeEntry(attributes, "TQ", showTq(d))
attributeEntry(attributes, "Entfernung", showDistance(d)) attributeEntry(attributes, "Entfernung", showDistance(d))
attributeEntry(attributes, "Typ", d.type) attributeEntry(attributes, "Typ", d.type)
var hw1 = dictGet(d.source.node.nodeinfo, ["hardware", "model"]) var hw1 = unknown ? null : dictGet(d.source.node.nodeinfo, ["hardware", "model"])
var hw2 = dictGet(d.target.node.nodeinfo, ["hardware", "model"]) var hw2 = dictGet(d.target.node.nodeinfo, ["hardware", "model"])
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)
config.linkInfos.forEach( function (linkInfo) {
var h4 = document.createElement("h4")
h4.textContent = linkInfo.name
el.appendChild(h4)
el.appendChild(showStatImg(linkInfo, d))
})
} }
}) })

View file

@ -1,92 +1,100 @@
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 h3lat = document.createElement("h3") var editLat = document.createElement("input")
h3lat.textContent = "Breitengrad" editLat.type = "text"
el.appendChild(h3lat) editLat.value = d.lat.toFixed(9)
var txt1 = document.createElement("textarea") el.appendChild(createBox("lat", "Breitengrad", editLat))
txt1.id = "location-latitude"
txt1.value = d.lat.toFixed(9)
var p = document.createElement("p")
p.appendChild(txt1)
p.appendChild(createCopyButton(txt1.id))
el.appendChild(p)
var h3lng = document.createElement("h3") var editLng = document.createElement("input")
h3lng.textContent = "Längengrad" editLng.type = "text"
el.appendChild(h3lng) editLng.value = d.lng.toFixed(9)
var txt2 = document.createElement("textarea") el.appendChild(createBox("lng", "Längengrad", editLng))
txt2.id = "location-longitude"
txt2.value = d.lng.toFixed(9)
var p2 = document.createElement("p")
p2.appendChild(txt2)
p2.appendChild(createCopyButton(txt2.id))
el.appendChild(p2)
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 = config.siteURL 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 = config.siteURL 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 = "Kopiere" 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 {
var successful = document.execCommand("copy") document.execCommand("copy")
var msg = successful ? "successful" : "unsuccessful"
console.log("Copying text command was " + msg)
} catch (err) { } catch (err) {
console.log("Oops, unable to copy") console.log(err)
} }
} }
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)
} }
function switch2uci() { function switch2uci() {
var box1 = document.getElementById("location-latitude") document.getElementById("box-uci").style.display = "block"
box1.value = "uci set gluon-node-info.@location[0].latitude='" + d.lat.toFixed(9) + "'" document.getElementById("box-lat").style.display = "none"
var box2 = document.getElementById("location-longitude") document.getElementById("box-lng").style.display = "none"
box2.value = "uci set gluon-node-info.@location[0].longitude='" + d.lng.toFixed(9) + "'"
} }
} }
}) })

View file

@ -42,7 +42,6 @@ define(["infobox/link", "infobox/node", "infobox/location"], function (Link, Nod
} }
self.gotoLocation = function (d) { self.gotoLocation = function (d) {
console.log("goto location called with ", d)
create() create()
new Location(config, el, router, d) new Location(config, el, router, d)
} }

View file

@ -38,8 +38,11 @@ define(["moment", "numeral", "tablesort", "tablesort.numeric"],
function showStatus(d) { function showStatus(d) {
return function (el) { return function (el) {
el.classList.add(d.flags.online ? "online" : "offline") el.classList.add(d.flags.unseen ? "unseen" : (d.flags.online ? "online" : "offline"))
el.textContent = d.flags.online ? "online" : "offline, " + d.lastseen.fromNow(true) + " " + d.lastseen.format("(DD.MM.YYYY, H:mm:ss)") if (d.flags.online)
el.textContent = "online, letzte Nachricht " + d.lastseen.fromNow() + " (" + d.lastseen.format("DD.MM.YYYY, H:mm:ss") + ")"
else
el.textContent = "offline, letzte Nachricht " + d.lastseen.fromNow() + " (" + d.lastseen.format("DD.MM.YYYY, H:mm:ss") + ")"
} }
} }
@ -78,18 +81,124 @@ define(["moment", "numeral", "tablesort", "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"))
} }
} }
@ -109,6 +218,9 @@ define(["moment", "numeral", "tablesort", "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)
@ -153,13 +265,13 @@ define(["moment", "numeral", "tablesort", "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
} }
function showLOAD(d) { function showLoad(d) {
if (!("loadavg" in d.statistics)) if (!("loadavg" in d.statistics))
return undefined return undefined
@ -177,6 +289,65 @@ define(["moment", "numeral", "tablesort", "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)
@ -216,46 +387,68 @@ define(["moment", "numeral", "tablesort", "tablesort.numeric"],
return au.enabled ? "aktiviert (" + au.branch + ")" : "deaktiviert" return au.enabled ? "aktiviert (" + au.branch + ")" : "deaktiviert"
} }
function showStatImg(o, nodeId) { function showNodeImg(o, model) {
var content, caption 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)
if (o.thumbnail) {
content = document.createElement("img") content = document.createElement("img")
content.src = o.thumbnail.replace("{NODE_ID}", nodeId) 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) { if (o.caption) {
caption = o.caption.replace("{NODE_ID}", nodeId) caption = o.caption.replace("{MODELHASH}", modelhash)
if (!content) if (!content)
content = document.createTextNode(caption) content = document.createTextNode(caption)
} }
var p = document.createElement("p") var p = document.createElement("p")
if (o.href) {
var link = document.createElement("a")
link.target = "_blank"
link.href = o.href.replace("{NODE_ID}", nodeId)
link.appendChild(content)
if (caption && o.thumbnail)
link.title = caption
p.appendChild(link)
} else
p.appendChild(content) p.appendChild(content)
return p return content
}
function showStatImg(o, d) {
var subst = {}
subst["{NODE_ID}"] = d.nodeinfo.node_id ? d.nodeinfo.node_id : "unknown"
subst["{NODE_NAME}"] = d.nodeinfo.hostname ? d.nodeinfo.hostname : "unknown"
return showStat(o, subst)
} }
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)
@ -271,13 +464,17 @@ define(["moment", "numeral", "tablesort", "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, "Load - avg", showLOAD(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, "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)
@ -287,7 +484,7 @@ define(["moment", "numeral", "tablesort", "tablesort.numeric"],
var h4 = document.createElement("h4") var h4 = document.createElement("h4")
h4.textContent = nodeInfo.name h4.textContent = nodeInfo.name
el.appendChild(h4) el.appendChild(h4)
el.appendChild(showStatImg(nodeInfo, d.nodeinfo.node_id)) el.appendChild(showStatImg(nodeInfo, d))
}) })
if (d.neighbours.length > 0) { if (d.neighbours.length > 0) {
@ -326,6 +523,7 @@ define(["moment", "numeral", "tablesort", "tablesort.numeric"],
var tbody = document.createElement("tbody") var tbody = document.createElement("tbody")
d.neighbours.forEach( function (d) { d.neighbours.forEach( function (d) {
var unknown = !(d.node)
var tr = document.createElement("tr") var tr = document.createElement("tr")
var td1 = document.createElement("td") var td1 = document.createElement("td")
@ -333,14 +531,9 @@ define(["moment", "numeral", "tablesort", "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 = d.node.nodeinfo.hostname
a1.href = "#"
a1.onclick = router.node(d.node)
td2.appendChild(a1)
if (has_location(d.node)) { if (!unknown && has_location(d.node)) {
var span = document.createElement("span") var span = document.createElement("span")
span.classList.add("icon") span.classList.add("icon")
span.classList.add("ion-location") span.classList.add("ion-location")

View file

@ -1,6 +1,6 @@
define(["sorttable", "virtual-dom"], function (SortTable, V) { define(["sorttable", "virtual-dom"], function (SortTable, V) {
function linkName(d) { function linkName(d) {
return d.source.node.nodeinfo.hostname + " " + d.target.node.nodeinfo.hostname return (d.source.node ? d.source.node.nodeinfo.hostname : d.source.id) + " " + d.target.node.nodeinfo.hostname
} }
var headings = [{ name: "Knoten", var headings = [{ name: "Knoten",

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
}) })
@ -61,15 +74,17 @@ function (moment, Router, L, GUI, numeral) {
var graph = dataGraph.batadv var graph = dataGraph.batadv
graph.nodes.forEach( function (d) { graph.nodes.forEach( function (d) {
if (d.node_id in graphnodes) if (d.node_id in graphnodes) {
d.node = graphnodes[d.node_id] d.node = graphnodes[d.node_id]
if (d.unseen) {
d.node.flags.online = true
d.node.flags.unseen = true
}
}
}) })
graph.links.forEach( function (d) { graph.links.forEach( function (d) {
if (graph.nodes[d.source].node)
d.source = graph.nodes[d.source] d.source = graph.nodes[d.source]
else
d.source = undefined
if (graph.nodes[d.target].node) if (graph.nodes[d.target].node)
d.target = graph.nodes[d.target] d.target = graph.nodes[d.target]
@ -78,14 +93,25 @@ function (moment, Router, L, GUI, numeral) {
}) })
var links = graph.links.filter( function (d) { var links = graph.links.filter( function (d) {
return d.source !== undefined && d.target !== undefined return d.target !== undefined
}) })
links.forEach( function (d) { links.forEach( function (d) {
var ids = [d.source.node.nodeinfo.node_id, d.target.node.nodeinfo.node_id] var unknown = (d.source.node === undefined)
d.id = ids.sort().join("-") var ids
if (unknown)
ids = [d.source.id.replace(/:/g, ""), d.target.node.nodeinfo.node_id]
else
ids = [d.source.node.nodeinfo.node_id, d.target.node.nodeinfo.node_id]
d.id = ids.join("-")
if (!("location" in d.source.node.nodeinfo && "location" in d.target.node.nodeinfo)) if (unknown ||
!d.source.node.nodeinfo.location ||
!d.target.node.nodeinfo.location ||
isNaN(d.source.node.nodeinfo.location.latitude) ||
isNaN(d.source.node.nodeinfo.location.longitude) ||
isNaN(d.target.node.nodeinfo.location.latitude) ||
isNaN(d.target.node.nodeinfo.location.longitude))
return return
d.latlngs = [] d.latlngs = []
@ -97,21 +123,53 @@ 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" || d.vpn) {
d.type = "VPN"
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.isVPN = false
} else if (d.type === "other") {
d.type = "Kabel"
d.isVPN = false
} else {
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)
if (unknown) {
d.target.node.neighbours.push({ id: d.source.id, link: d, incoming: true })
return
}
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 !== "tunnel") 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
if (d.type === "tunnel")
d.type = "VPN"
else if (d.type === "wireless")
d.type = "Wifi"
else if (d.type === "other")
d.type = "Kabel"
else
d.type = "N/A"
}) })
links.sort( function (a, b) { links.sort( function (a, b) {

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,23 +259,22 @@ 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
} }
} }
function contextMenuGotoLocation(e) { function contextMenuGotoLocation(e) {
console.log("context menu called at ", e)
router.gotoLocation(e.latlng) router.gotoLocation(e.latlng)
} }
@ -287,7 +286,7 @@ 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)
} }
}) })
@ -319,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}) var clientLayer = new ClientLayer({minZoom: 15})
clientLayer.addTo(map) clientLayer.addTo(map)
clientLayer.setZIndex(5) clientLayer.setZIndex(5)
var labelsLayer = new LabelsLayer() 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
@ -448,7 +464,10 @@ define(["map/clientlayer", "map/labelslayer",
var lines = addLinksToMap(linkDict, linkScale, data.graph.links, router) var lines = addLinksToMap(linkDict, linkScale, data.graph.links, router)
groupLines = L.featureGroup(lines).addTo(map) groupLines = L.featureGroup(lines).addTo(map)
if (typeof config.fixedCenter === "undefined")
barycenter = calcBarycenter(data.nodes.all.filter(has_location)) barycenter = calcBarycenter(data.nodes.all.filter(has_location))
else
barycenter = L.circle(L.latLng(new L.LatLng(config.fixedCenter.lat, config.fixedCenter.lng)), config.fixedCenter.radius * 1000)
var nodesOnline = subtract(data.nodes.all.filter(online), data.nodes.new) var nodesOnline = subtract(data.nodes.all.filter(online), data.nodes.new)
var nodesOffline = subtract(data.nodes.all.filter(offline), data.nodes.lost) var nodesOffline = subtract(data.nodes.all.filter(offline), data.nodes.lost)

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

@ -23,43 +23,17 @@ define(["chroma-js", "virtual-dom", "numeral-intl", "filters/genericnode", "verc
var uplinkTable = document.createElement("table") var uplinkTable = document.createElement("table")
uplinkTable.classList.add("proportion") uplinkTable.classList.add("proportion")
var gwTable = document.createElement("table") var gwNodesTable = document.createElement("table")
gwTable.classList.add("proportion") gwNodesTable.classList.add("proportion")
var gwClientsTable = document.createElement("table")
gwClientsTable.classList.add("proportion")
var siteTable = document.createElement("table") var siteTable = document.createElement("table")
siteTable.classList.add("proportion") siteTable.classList.add("proportion")
function showStatGlobal(o) { function showStatGlobal(o) {
var content, caption return showStat(o)
if (o.thumbnail) {
content = document.createElement("img")
content.src = o.thumbnail
}
if (o.caption) {
caption = o.caption
if (!content)
content = document.createTextNode(caption)
}
var p = document.createElement("p")
if (o.href) {
var link = document.createElement("a")
link.target = "_blank"
link.href = o.href
link.appendChild(content)
if (caption && o.thumbnail)
link.title = caption
p.appendChild(link)
} else
p.appendChild(content)
return p
} }
function count(nodes, key, f) { function count(nodes, key, f) {
@ -80,6 +54,25 @@ define(["chroma-js", "virtual-dom", "numeral-intl", "filters/genericnode", "verc
return Object.keys(dict).map(function (d) { return [d, dict[d], key, f] }) return Object.keys(dict).map(function (d) { return [d, dict[d], key, f] })
} }
function countClients(nodes, key, f) {
var dict = {}
nodes.forEach( function (d) {
var v = dictGet(d, key.slice(0))
if (f !== undefined)
v = f(v)
if (v === null)
return
dict[v] = d.statistics.clients + (v in dict ? dict[v] : 0)
})
return Object.keys(dict).map(function (d) { return [d, dict[d], key, f] })
}
function addFilter(filter) { function addFilter(filter) {
return function () { return function () {
filterManager.addFilter(filter) filterManager.addFilter(filter)
@ -135,10 +128,17 @@ 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) {
var geoDict = count(nodes, ["nodeinfo", "location"], function (d) { if (d) {
return d ? "ja" : "nein" 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) {
return d && d.longitude && d.latitude ? "ja" : "nein"
})
var autoDict = count(nodes, ["nodeinfo", "software", "autoupdater"], function (d) { var autoDict = count(nodes, ["nodeinfo", "software", "autoupdater"], function (d) {
if (d === null) if (d === null)
return null return null
@ -147,21 +147,38 @@ define(["chroma-js", "virtual-dom", "numeral-intl", "filters/genericnode", "verc
else else
return "(deaktiviert)" return "(deaktiviert)"
}) })
var uplinkDict = count(nodes, ["flags", "uplink"], function (d) { var uplinkDict = count(nodes, ["flags", "uplink"], function (d) {
return d ? "ja" : "nein" return d ? "ja" : "nein"
}) })
var gwDict = 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
}) })
var siteDict = count(onlineNodes, ["nodeinfo", "system", "site_code"], function (d) { var gwClientsDict = countClients(onlineNodes, ["statistics", "gateway"], function (d) {
if (d === null)
return null
if (d.node)
return d.node.nodeinfo.hostname
if (d.id)
return d.id
return 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) {
@ -175,71 +192,45 @@ define(["chroma-js", "virtual-dom", "numeral-intl", "filters/genericnode", "verc
fillTable("Firmware", fwTable, fwDict.sort(function (a, b) { return vercomp(b[0], a[0]) })) fillTable("Firmware", fwTable, fwDict.sort(function (a, b) { return vercomp(b[0], a[0]) }))
fillTable("Hardware", hwTable, hwDict.sort(function (a, b) { return b[1] - a[1] })) fillTable("Hardware", hwTable, hwDict.sort(function (a, b) { return b[1] - a[1] }))
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("Autom. Updates", autoTable, autoDict.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("Gewähltes Gateway", gwTable, gwDict.sort(function (a, b) { return b[1] - a[1] })) fillTable("Autom. Updates", autoTable, autoDict.sort(function (a, b) { return b[1] - a[1] }))
fillTable("Gateway", gwNodesTable, gwNodesDict.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] }))
} }
self.render = function (el) { self.render = function (el) {
var h2 var h2
h2 = document.createElement("h2") self.renderSingle(el, "Status", statusTable)
h2.textContent = "Status" self.renderSingle(el, "Nodes an Gateway", gwNodesTable)
el.appendChild(h2) self.renderSingle(el, "Clients an Gateway", gwClientsTable)
el.appendChild(statusTable) self.renderSingle(el, "Firmwareversionen", fwTable)
self.renderSingle(el, "Uplink", uplinkTable)
h2 = document.createElement("h2") self.renderSingle(el, "Hardwaremodelle", hwTable)
h2.textContent = "Firmwareversionen" self.renderSingle(el, "Auf der Karte sichtbar", geoTable)
el.appendChild(h2) self.renderSingle(el, "Autoupdater", autoTable)
el.appendChild(fwTable) self.renderSingle(el, "Site", siteTable)
if(config.siteNames || config.showSites) {
h2 = document.createElement("h2")
h2.textContent = "Orte"
el.appendChild(h2)
el.appendChild(siteTable)
}
h2 = document.createElement("h2")
h2.textContent = "Hardwaremodelle"
el.appendChild(h2)
el.appendChild(hwTable)
h2 = document.createElement("h2")
h2.textContent = "Auf der Karte sichtbar"
el.appendChild(h2)
el.appendChild(geoTable)
h2 = document.createElement("h2")
h2.textContent = "Autoupdater"
el.appendChild(h2)
el.appendChild(autoTable)
h2 = document.createElement("h2")
h2.textContent = "Uplink"
el.appendChild(h2)
el.appendChild(uplinkTable)
h2 = document.createElement("h2")
h2.textContent = "Gewählter Gateway"
el.appendChild(h2)
el.appendChild(gwTable)
h2 = document.createElement("h2")
h2.textContent = "Site"
el.appendChild(h2)
el.appendChild(siteTable)
if (config.globalInfos) if (config.globalInfos)
config.globalInfos.forEach(function (globalInfo) { config.globalInfos.forEach(function (globalInfo) {
h2 = document.createElement("h2") h2 = document.createElement("h2")
h2.textContent = globalInfo.name h2.textContent = globalInfo.name
el.appendChild(h2) el.appendChild(h2)
el.appendChild(showStatGlobal(globalInfo)) el.appendChild(showStatGlobal(globalInfo))
}) })
} }
self.renderSingle = function (el, heading, table) {
var h2
h2 = document.createElement("h2")
h2.textContent = heading
h2.onclick = function () {
table.classList.toggle("hidden")
}
el.appendChild(h2)
el.appendChild(table)
}
return self return self
} }
}) })

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

@ -20,7 +20,7 @@ define(function () {
this.gotoLink = function (d) { this.gotoLink = function (d) {
if (d) if (d)
setTitle(d.source.node.nodeinfo.hostname + " " + d.target.node.nodeinfo.hostname) setTitle((d.source.node ? d.source.node.nodeinfo.hostname : d.source.id) + " " + d.target.node.nodeinfo.hostname)
} }
this.gotoLocation = function() { this.gotoLocation = function() {

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": {
@ -26,6 +45,7 @@
"node": true "node": true
}, },
"globals": { "globals": {
"showStat": false,
"attributeEntry": false, "attributeEntry": false,
"dictGet": false, "dictGet": false,
"getJSON": false, "getJSON": false,

View file

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

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;
@ -44,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);
} }
@ -69,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 {
@ -116,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);
@ -127,6 +161,10 @@ table.attributes td {
display: none; display: none;
} }
.container table.hidden {
display: none;
}
p { p {
line-height: 1.67em; line-height: 1.67em;
} }
@ -139,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;
@ -147,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 {
@ -155,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;
@ -175,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 {
@ -194,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;
@ -284,6 +353,10 @@ table {
color: #D43E2A !important; color: #D43E2A !important;
} }
.unseen {
color: #D89100 !important;
}
.sidebar { .sidebar {
z-index: 5; z-index: 5;
width: $sidebarwidth; width: $sidebarwidth;
@ -291,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;
} }
@ -376,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);
@ -409,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,53 +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/"
} }
}, },
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,
@ -103,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