From 8b5bcbfede181689f5ed43110c0bf0616eed9ad5 Mon Sep 17 00:00:00 2001
From: H4ndl3 <beier.florian@gmail.com>
Date: Mon, 13 Mar 2017 22:54:36 +0100
Subject: [PATCH 01/32] 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
---
 html/index.html   | 10 ++++++++++
 index.html        | 10 ++++++++++
 lib/gui.js        |  3 +++
 scss/_base.scss   |  4 ++++
 scss/_loader.scss | 23 +++++++++++++++++++++++
 scss/main.scss    |  1 +
 6 files changed, 51 insertions(+)
 create mode 100644 scss/_loader.scss

diff --git a/html/index.html b/html/index.html
index fc4ff6e..6f5c4c7 100644
--- a/html/index.html
+++ b/html/index.html
@@ -14,5 +14,15 @@
     </script>
   </head>
   <body>
+    <div class="loader">
+    <p>
+      Lade<br />
+      <span class="spinner"></span><br />
+      Karte &amp; Knoten...
+    </p>
+    <noscript>
+      <strong>JavaScript required</strong>
+    </noscript>
+    </div>
   </body>
 </html>
diff --git a/index.html b/index.html
index fcbf858..76dfc6e 100644
--- a/index.html
+++ b/index.html
@@ -13,5 +13,15 @@
     <script src="bower_components/requirejs/require.js" data-main="app"></script>
   </head>
   <body>
+    <div class="loader">
+      <p>
+        Lade<br/>
+        <span class="spinner"></span><br \>
+        Karte &amp; Knoten...
+      </p>
+      <noscript>
+        <strong>JavaScript required</strong>
+      </noscript>
+    </div>
   </body>
 </html>
diff --git a/lib/gui.js b/lib/gui.js
index 4ce989d..4e14c50 100644
--- a/lib/gui.js
+++ b/lib/gui.js
@@ -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.classList.add("content")
     document.body.appendChild(contentDiv)
diff --git a/scss/_base.scss b/scss/_base.scss
index 52f18c3..21ca3ff 100644
--- a/scss/_base.scss
+++ b/scss/_base.scss
@@ -28,3 +28,7 @@ h5 {
 h6 {
   font-size: 0.67em;
 }
+
+.hide {
+  display: none;
+}
diff --git a/scss/_loader.scss b/scss/_loader.scss
new file mode 100644
index 0000000..d56c27e
--- /dev/null
+++ b/scss/_loader.scss
@@ -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);
+  }
+}
diff --git a/scss/main.scss b/scss/main.scss
index 34fd594..47cbed9 100644
--- a/scss/main.scss
+++ b/scss/main.scss
@@ -4,6 +4,7 @@
 @import '_leaflet';
 @import '_leaflet.label';
 @import '_filters';
+@import '_loader';
 
 $minscreenwidth: 630pt;
 $sidebarwidth: 420pt;

From eb4c7a04b099e0c3abf8c16b9e2cc0117981273f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Milan=20P=C3=A4ssler?= <me@petabyteboy.de>
Date: Fri, 17 Mar 2017 03:14:57 +0100
Subject: [PATCH 02/32] change code style to ffrgb/meshviewer fork

---
 .editorconfig              |    2 +-
 .gitignore                 |    2 +
 Gruntfile.js               |   24 +-
 app.js                     |    6 +-
 build.js                   |   16 +-
 helper.js                  |  249 ++++----
 html/index.html            |   50 +-
 index.html                 |   48 +-
 lib/about.js               |   56 +-
 lib/container.js           |   23 +-
 lib/datadistributor.js     |  149 ++---
 lib/filters/filtergui.js   |   57 +-
 lib/filters/genericnode.js |   62 +-
 lib/filters/nodefilter.js  |   49 +-
 lib/forcegraph.js          | 1072 ++++++++++++++++----------------
 lib/gui.js                 |  200 +++---
 lib/infobox/link.js        |   77 +--
 lib/infobox/location.js    |  141 ++---
 lib/infobox/main.js        |   60 +-
 lib/infobox/node.js        | 1190 +++++++++++++++++++-----------------
 lib/legend.js              |   64 +-
 lib/linklist.js            |   96 +--
 lib/locationmarker.js      |   40 +-
 lib/main.js                |  411 +++++++------
 lib/map.js                 |  716 ++++++++++++----------
 lib/map/clientlayer.js     |   89 +--
 lib/map/labelslayer.js     |  264 ++++----
 lib/meshstats.js           |   92 +--
 lib/nodelist.js            |  154 ++---
 lib/proportions.js         |  446 ++++++++------
 lib/router.js              |  258 ++++----
 lib/sidebar.js             |   59 +-
 lib/simplenodelist.js      |   83 +--
 lib/sorttable.js           |   69 ++-
 lib/tabs.js                |   70 ++-
 lib/title.js               |   41 +-
 lib/vercomp.js             |   78 +--
 scss/main.scss             |    6 +-
 tasks/build.js             |   68 +--
 tasks/clean.js             |    6 +-
 tasks/development.js       |    8 +-
 tasks/linting.js           |    8 +-
 42 files changed, 3505 insertions(+), 3154 deletions(-)

diff --git a/.editorconfig b/.editorconfig
index f0abb4f..604c949 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -11,6 +11,6 @@ trim_trailing_whitespace = true
 end_of_line = lf
 insert_final_newline = true
 
-[*.{js,html,scss,json}]
+[*.{js,html,scss,json,yml,md}]
 indent_size = 2
 indent_style = space
diff --git a/.gitignore b/.gitignore
index 1f46137..e728065 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,5 @@ node_modules/
 build/
 .sass-cache/
 config.json
+.idea/
+.eslintrc
diff --git a/Gruntfile.js b/Gruntfile.js
index b63ad56..6fcc054 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -1,24 +1,24 @@
 module.exports = function (grunt) {
-  grunt.loadNpmTasks("grunt-git-describe")
+  grunt.loadNpmTasks("grunt-git-describe");
 
   grunt.initConfig({
     "git-describe": {
       options: {},
       default: {}
     }
-  })
+  });
 
-  grunt.registerTask("saveRevision", function() {
+  grunt.registerTask("saveRevision", function () {
     grunt.event.once("git-describe", function (rev) {
-      grunt.option("gitRevision", rev)
-    })
-    grunt.task.run("git-describe")
-  })
+      grunt.option("gitRevision", rev);
+    });
+    grunt.task.run("git-describe");
+  });
 
-  grunt.loadTasks("tasks")
+  grunt.loadTasks("tasks");
 
-  grunt.registerTask("default", ["bower-install-simple", "lint", "saveRevision", "copy", "sass", "postcss", "requirejs"])
-  grunt.registerTask("lint", ["eslint"])
-  grunt.registerTask("dev", ["default", "connect:server", "watch"])
-}
+  grunt.registerTask("default", ["bower-install-simple", "lint", "saveRevision", "copy", "sass", "postcss", "requirejs"]);
+  grunt.registerTask("lint", ["eslint"]);
+  grunt.registerTask("dev", ["default", "connect:server", "watch"]);
+};
 
diff --git a/app.js b/app.js
index e848121..ef131ef 100644
--- a/app.js
+++ b/app.js
@@ -29,8 +29,8 @@ require.config({
     "tablesort.numeric": ["tablesort"],
     "helper": ["numeral-intl"]
   }
-})
+});
 
 require(["main", "helper"], function (main) {
-  getJSON("config.json").then(main)
-})
+  getJSON("config.json").then(main);
+});
diff --git a/build.js b/build.js
index 021929a..41f588b 100644
--- a/build.js
+++ b/build.js
@@ -1,9 +1,9 @@
 ({
-    baseUrl: "lib",
-    name: "../bower_components/almond/almond",
-    mainConfigFile: "app.js",
-    include: "../app",
-    wrap: true,
-    optimize: "uglify",
-    out: "app-combined.js"
-})
+  baseUrl: "lib",
+  name: "../bower_components/almond/almond",
+  mainConfigFile: "app.js",
+  include: "../app",
+  wrap: true,
+  optimize: "uglify",
+  out: "app-combined.js"
+});
diff --git a/helper.js b/helper.js
index 6e00702..1237021 100644
--- a/helper.js
+++ b/helper.js
@@ -1,228 +1,241 @@
 function get(url) {
-  return new Promise(function(resolve, reject) {
-    var req = new XMLHttpRequest()
-    req.open('GET', url)
+  return new Promise(function (resolve, reject) {
+    var req = new XMLHttpRequest();
+    req.open('GET', url);
 
-    req.onload = function() {
+    req.onload = function () {
       if (req.status == 200) {
-        resolve(req.response)
+        resolve(req.response);
       }
       else {
-        reject(Error(req.statusText))
+        reject(Error(req.statusText));
       }
-    }
+    };
 
-    req.onerror = function() {
-      reject(Error("Network Error"))
-    }
+    req.onerror = function () {
+      reject(Error("Network Error"));
+    };
 
-    req.send()
-  })
+    req.send();
+  });
 }
 
 function getJSON(url) {
-  return get(url).then(JSON.parse)
+  return get(url).then(JSON.parse);
 }
 
 function sortByKey(key, d) {
-  return d.slice().sort( function (a, b) {
-    return a[key] - b[key]
-  }).reverse()
+  return d.slice().sort(function (a, b) {
+    return a[key] - b[key];
+  }).reverse();
 }
 
 function limit(key, m, d) {
-  return d.filter( function (d) {
-    return d[key].isAfter(m)
-  })
+  return d.filter(function (d) {
+    return d[key].isAfter(m);
+  });
 }
 
 function sum(a) {
-  return a.reduce( function (a, b) {
-    return a + b
-  }, 0)
+  return a.reduce(function (a, b) {
+    return a + b;
+  }, 0);
 }
 
 function one() {
-  return 1
+  return 1;
 }
 
 function trueDefault(d) {
-  return d === undefined ? true : d
+  return d === undefined ? true : d;
 }
 
 function dictGet(dict, key) {
-  var k = key.shift()
+  var k = key.shift();
 
-  if (!(k in dict))
-    return null
+  if (!(k in dict)) {
+    return null;
+  }
 
-  if (key.length == 0)
-    return dict[k]
+  if (key.length == 0) {
+    return dict[k];
+  }
 
-  return dictGet(dict[k], key)
+  return dictGet(dict[k], key);
 }
 
 function localStorageTest() {
-  var test = 'test'
+  var test = 'test';
   try {
-    localStorage.setItem(test, test)
-    localStorage.removeItem(test)
-    return true
-  } catch(e) {
-    return false
+    localStorage.setItem(test, test);
+    localStorage.removeItem(test);
+    return true;
+  } catch (e) {
+    return false;
   }
 }
 
 function listReplace(s, subst) {
   for (key in subst) {
-    var re = new RegExp(key, 'g')
-    s = s.replace(re, subst[key])
+    var re = new RegExp(key, 'g');
+    s = s.replace(re, subst[key]);
   }
-  return s
+  return s;
 }
 
 /* Helpers working with nodes */
 
 function offline(d) {
-  return !d.flags.online
+  return !d.flags.online;
 }
 
 function online(d) {
-  return d.flags.online
+  return d.flags.online;
 }
 
 function has_location(d) {
   return "location" in d.nodeinfo &&
-         Math.abs(d.nodeinfo.location.latitude) < 90 &&
-         Math.abs(d.nodeinfo.location.longitude) < 180
+    Math.abs(d.nodeinfo.location.latitude) < 90 &&
+    Math.abs(d.nodeinfo.location.longitude) < 180;
 }
 
 function subtract(a, b) {
-  var ids = {}
+  var ids = {};
 
-  b.forEach( function (d) {
-    ids[d.nodeinfo.node_id] = true
-  })
+  b.forEach(function (d) {
+    ids[d.nodeinfo.node_id] = true;
+  });
 
-  return a.filter( function (d) {
-    return !(d.nodeinfo.node_id in ids)
-  })
+  return a.filter(function (d) {
+    return !(d.nodeinfo.node_id in ids);
+  });
 }
 
 /* Helpers working with links */
 
 function showDistance(d) {
-  if (isNaN(d.distance))
-    return
+  if (isNaN(d.distance)) {
+    return;
+  }
 
-  return numeral(d.distance).format("0,0") + " m"
+  return numeral(d.distance).format("0,0") + " m";
 }
 
 function showTq(d) {
-  return numeral(1/d.tq).format("0%")
+  return numeral(1 / d.tq).format("0%");
 }
 
 /* Infobox stuff (XXX: move to module) */
 
 function attributeEntry(el, label, value) {
-  if (value === null || value == undefined)
-    return
-
-  var tr = document.createElement("tr")
-  var th = document.createElement("th")
-  if (typeof label === "string")
-    th.textContent = label
-  else {
-    th.appendChild(label)
-    tr.className = "routerpic"
+  if (value === null || value == undefined) {
+    return;
   }
 
-  tr.appendChild(th)
+  var tr = document.createElement("tr");
+  var th = document.createElement("th");
+  if (typeof label === "string") {
+    th.textContent = label;
+  } else {
+    th.appendChild(label);
+    tr.className = "routerpic";
+  }
 
-  var td = document.createElement("td")
+  tr.appendChild(th);
 
-  if (typeof value == "function")
-    value(td)
-  else
-    td.appendChild(document.createTextNode(value))
+  var td = document.createElement("td");
 
-  tr.appendChild(td)
+  if (typeof value == "function") {
+    value(td);
+  } else {
+    td.appendChild(document.createTextNode(value));
+  }
 
-  el.appendChild(tr)
+  tr.appendChild(td);
 
-  return td
+  el.appendChild(tr);
+
+  return td;
 }
 
 function createIframe(opt, width, height) {
-  el = document.createElement("iframe")
-  width = typeof width !== 'undefined' ? width : '100%'
-  height = typeof height !== 'undefined' ? height : '350px'
+  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.src) {
+    el.src = opt.src;
+  } else {
+    el.src = opt;
+  }
 
-  if (opt.frameBorder)
-    el.frameBorder = opt.frameBorder
-  else
-    el.frameBorder = 1
+  if (opt.frameBorder) {
+    el.frameBorder = opt.frameBorder;
+  } else {
+    el.frameBorder = 1;
+  }
 
-  if (opt.width)
-    el.width = opt.width
-  else
-    el.width = width
+  if (opt.width) {
+    el.width = opt.width;
+  } else {
+    el.width = width;
+  }
 
-  if (opt.height)
-    el.height = opt.height
-  else
-    el.height = height
+  if (opt.height) {
+    el.height = opt.height;
+  } else {
+    el.height = height;
+  }
 
-  el.scrolling = "no"
-  el.seamless = "seamless"
+  el.scrolling = "no";
+  el.seamless = "seamless";
 
-  return el
+  return el;
 }
 
 function showStat(o, subst) {
-  var content, caption
-  subst = typeof subst !== 'undefined' ? subst : {}
+  var content, caption;
+  subst = typeof subst !== 'undefined' ? subst : {};
 
   if (o.thumbnail) {
-    content = document.createElement("img")
-    content.src = listReplace(o.thumbnail, subst)
+    content = document.createElement("img");
+    content.src = listReplace(o.thumbnail, subst);
   }
 
   if (o.caption) {
-    caption = listReplace(o.caption, subst)
+    caption = listReplace(o.caption, subst);
 
-    if (!content)
-    content = document.createTextNode(caption)
+    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)
+    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")
+  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)
+    var link = document.createElement("a");
+    link.target = "_blank";
+    link.href = listReplace(o.href, subst);
+    link.appendChild(content);
 
-    if (caption && o.thumbnail)
-    link.title = caption
+    if (caption && o.thumbnail) {
+      link.title = caption;
+    }
 
-    p.appendChild(link)
-  } else
-    p.appendChild(content)
+    p.appendChild(link);
+  } else {
+    p.appendChild(content);
+  }
 
-  return p
+  return p;
 }
 
diff --git a/html/index.html b/html/index.html
index 6f5c4c7..cc95aa5 100644
--- a/html/index.html
+++ b/html/index.html
@@ -1,28 +1,28 @@
 <!DOCTYPE html>
 <html>
-  <head>
-    <meta charset="utf-8">
-    <meta name="viewport" content="width=device-width, user-scalable=no">
-    <link rel="stylesheet" href="css/ionicons.min.css">
-    <link rel="stylesheet" href="roboto-slab-fontface.css">
-    <link rel="stylesheet" href="roboto-fontface.css">
-    <link rel="stylesheet" href="style.css">
-    <script src="vendor/es6-shim/es6-shim.min.js"></script>
-    <script src="app.js"></script>
-    <script>
-      console.log("Version: #revision#")
-    </script>
-  </head>
-  <body>
-    <div class="loader">
-    <p>
-      Lade<br />
-      <span class="spinner"></span><br />
-      Karte &amp; Knoten...
-    </p>
-    <noscript>
-      <strong>JavaScript required</strong>
-    </noscript>
-    </div>
-  </body>
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, user-scalable=no">
+  <link rel="stylesheet" href="css/ionicons.min.css">
+  <link rel="stylesheet" href="roboto-slab-fontface.css">
+  <link rel="stylesheet" href="roboto-fontface.css">
+  <link rel="stylesheet" href="style.css">
+  <script src="vendor/es6-shim/es6-shim.min.js"></script>
+  <script src="app.js"></script>
+  <script>
+    console.log("Version: #revision#");
+  </script>
+</head>
+<body>
+<div class="loader">
+  <p>
+    Lade<br/>
+    <span class="spinner"></span><br/>
+    Karte &amp; Knoten...
+  </p>
+  <noscript>
+    <strong>JavaScript required</strong>
+  </noscript>
+</div>
+</body>
 </html>
diff --git a/index.html b/index.html
index 76dfc6e..96935bc 100644
--- a/index.html
+++ b/index.html
@@ -1,27 +1,27 @@
 <!DOCTYPE html>
 <html>
-  <head>
-    <meta charset="utf-8">
-    <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="bower_components/roboto-fontface/roboto-fontface.css">
-    <link rel="stylesheet" href="bower_components/leaflet/dist/leaflet.css">
-    <link rel="stylesheet" href="bower_components/Leaflet.label/dist/leaflet.label.css">
-    <link rel="stylesheet" href="bower_components/ionicons/css/ionicons.min.css">
-    <link rel="stylesheet" href="style.css">
-    <script src="bower_components/es6-shim/es6-shim.min.js"></script>
-    <script src="bower_components/requirejs/require.js" data-main="app"></script>
-  </head>
-  <body>
-    <div class="loader">
-      <p>
-        Lade<br/>
-        <span class="spinner"></span><br \>
-        Karte &amp; Knoten...
-      </p>
-      <noscript>
-        <strong>JavaScript required</strong>
-      </noscript>
-    </div>
-  </body>
+<head>
+  <meta charset="utf-8">
+  <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="bower_components/roboto-fontface/roboto-fontface.css">
+  <link rel="stylesheet" href="bower_components/leaflet/dist/leaflet.css">
+  <link rel="stylesheet" href="bower_components/Leaflet.label/dist/leaflet.label.css">
+  <link rel="stylesheet" href="bower_components/ionicons/css/ionicons.min.css">
+  <link rel="stylesheet" href="style.css">
+  <script src="bower_components/es6-shim/es6-shim.min.js"></script>
+  <script src="bower_components/requirejs/require.js" data-main="app"></script>
+</head>
+<body>
+  <div class="loader">
+  <p>
+    Lade<br/>
+    <span class="spinner"></span><br \>
+    Karte &amp; Knoten...
+  </p>
+  <noscript>
+    <strong>JavaScript required</strong>
+  </noscript>
+  </div>
+</body>
 </html>
diff --git a/lib/about.js b/lib/about.js
index 6a44b78..2a20ae5 100644
--- a/lib/about.js
+++ b/lib/about.js
@@ -1,38 +1,38 @@
 define(function () {
-  return function() {
+  return function () {
     this.render = function (d) {
-      var el = document.createElement("div")
-      d.appendChild(el)
-      var s = "<h2>Über HopGlass</h2>"
+      var el = document.createElement("div");
+      d.appendChild(el);
+      var s = "<h2>Über HopGlass</h2>";
 
-      s += "<p>Mit Doppelklick und Shift+Doppelklick kann man in der Karte "
-      s += "auch zoomen.</p>"
+      s += "<p>Mit Doppelklick und Shift+Doppelklick kann man in der Karte ";
+      s += "auch zoomen.</p>";
 
-      s += "<h3>AGPL 3</h3>"
+      s += "<h3>AGPL 3</h3>";
 
-      s += "<p>Copyright (C) Milan Pässler</p>"
-      s += "<p>Copyright (C) Nils Schneider</p>"
+      s += "<p>Copyright (C) Milan Pässler</p>";
+      s += "<p>Copyright (C) Nils Schneider</p>";
 
-      s += "<p>This program is free software: you can redistribute it and/or "
-      s += "modify it under the terms of the GNU Affero General Public "
-      s += "License as published by the Free Software Foundation, either "
-      s += "version 3 of the License, or (at your option) any later version.</p>"
+      s += "<p>This program is free software: you can redistribute it and/or ";
+      s += "modify it under the terms of the GNU Affero General Public ";
+      s += "License as published by the Free Software Foundation, either ";
+      s += "version 3 of the License, or (at your option) any later version.</p>";
 
-      s += "<p>This program is distributed in the hope that it will be useful, "
-      s += "but WITHOUT ANY WARRANTY; without even the implied warranty of "
-      s += "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the "
-      s += "GNU Affero General Public License for more details.</p>"
+      s += "<p>This program is distributed in the hope that it will be useful, ";
+      s += "but WITHOUT ANY WARRANTY; without even the implied warranty of ";
+      s += "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the ";
+      s += "GNU Affero General Public License for more details.</p>";
 
-      s += "<p>You should have received a copy of the GNU Affero General "
-      s += "Public License along with this program. If not, see "
-      s += "<a href=\"https://www.gnu.org/licenses/\">"
-      s += "https://www.gnu.org/licenses/</a>.</p>"
+      s += "<p>You should have received a copy of the GNU Affero General ";
+      s += "Public License along with this program. If not, see ";
+      s += "<a href=\"https://www.gnu.org/licenses/\">";
+      s += "https://www.gnu.org/licenses/</a>.</p>";
 
-      s += "<p>The source code is available at "
-      s += "<a href=\"https://github.com/plumpudding/hopglass\">"
-      s += "https://github.com/plumpudding/hopglass</a>."
+      s += "<p>The source code is available at ";
+      s += "<a href=\"https://github.com/plumpudding/hopglass\">";
+      s += "https://github.com/plumpudding/hopglass</a>.";
 
-      el.innerHTML = s
-    }
-  }
-})
+      el.innerHTML = s;
+    };
+  };
+});
diff --git a/lib/container.js b/lib/container.js
index a33ab75..4f32a84 100644
--- a/lib/container.js
+++ b/lib/container.js
@@ -1,20 +1,21 @@
 define([], function () {
   return function (tag) {
-    if (!tag)
-      tag = "div"
+    if (!tag) {
+      tag = "div";
+    }
 
-    var self = this
+    var self = this;
 
-    var container = document.createElement(tag)
+    var container = document.createElement(tag);
 
     self.add = function (d) {
-      d.render(container)
-    }
+      d.render(container);
+    };
 
     self.render = function (el) {
-      el.appendChild(container)
-    }
+      el.appendChild(container);
+    };
 
-    return self
-  }
-})
+    return self;
+  };
+});
diff --git a/lib/datadistributor.js b/lib/datadistributor.js
index 67cf7eb..3edeaf6 100644
--- a/lib/datadistributor.js
+++ b/lib/datadistributor.js
@@ -1,80 +1,91 @@
 define(["filters/nodefilter"], function (NodeFilter) {
   return function () {
-    var targets = []
-    var filterObservers = []
-    var filters = []
-    var filteredData
-    var data
+    var targets = [];
+    var filterObservers = [];
+    var filters = [];
+    var filteredData;
+    var data;
 
     function remove(d) {
-      targets = targets.filter( function (e) { return d !== e } )
+      targets = targets.filter(function (e) {
+        return d !== e;
+      });
     }
 
     function add(d) {
-      targets.push(d)
+      targets.push(d);
 
-      if (filteredData !== undefined)
-        d.setData(filteredData)
-    }
-
-    function setData(d) {
-      data = d
-      refresh()
-    }
-
-    function refresh() {
-      if (data === undefined)
-        return
-
-      var filter = filters.reduce( function (a, f) {
-        return function (d) {
-          return a(d) && f.run(d)
-        }
-      }, function () { return true })
-
-      filteredData = new NodeFilter(filter)(data)
-
-      targets.forEach( function (t) {
-        t.setData(filteredData)
-      })
-    }
-
-    function notifyObservers() {
-      filterObservers.forEach( function (d) {
-        d.filtersChanged(filters)
-      })
-    }
-
-    function addFilter(d) {
-      filters.push(d)
-      notifyObservers()
-      d.setRefresh(refresh)
-      refresh()
-    }
-
-    function removeFilter(d) {
-      filters = filters.filter( function (e) { return d !== e } )
-      notifyObservers()
-      refresh()
-    }
-
-    function watchFilters(d) {
-      filterObservers.push(d)
-
-      d.filtersChanged(filters)
-
-      return function () {
-        filterObservers = filterObservers.filter( function (e) { return d !== e })
+      if (filteredData !== undefined) {
+        d.setData(filteredData);
       }
     }
 
-    return { add: add,
-             remove: remove,
-             setData: setData,
-             addFilter: addFilter,
-             removeFilter: removeFilter,
-             watchFilters: watchFilters,
-             refresh: refresh
-           }
-  }
-})
+    function setData(d) {
+      data = d;
+      refresh();
+    }
+
+    function refresh() {
+      if (data === undefined) {
+        return;
+      }
+
+      var filter = filters.reduce(function (a, f) {
+        return function (d) {
+          return a(d) && f.run(d);
+        };
+      }, function () {
+        return true;
+      });
+
+      filteredData = new NodeFilter(filter)(data);
+
+      targets.forEach(function (t) {
+        t.setData(filteredData);
+      });
+    }
+
+    function notifyObservers() {
+      filterObservers.forEach(function (d) {
+        d.filtersChanged(filters);
+      });
+    }
+
+    function addFilter(d) {
+      filters.push(d);
+      notifyObservers();
+      d.setRefresh(refresh);
+      refresh();
+    }
+
+    function removeFilter(d) {
+      filters = filters.filter(function (e) {
+        return d !== e;
+      });
+      notifyObservers();
+      refresh();
+    }
+
+    function watchFilters(d) {
+      filterObservers.push(d);
+
+      d.filtersChanged(filters);
+
+      return function () {
+        filterObservers = filterObservers.filter(function (e) {
+          return d !== e;
+        });
+      };
+    }
+
+    return {
+      add: add,
+      remove: remove,
+      setData: setData,
+      addFilter: addFilter,
+      removeFilter: removeFilter,
+      watchFilters: watchFilters,
+      refresh: refresh
+    };
+  };
+});
diff --git a/lib/filters/filtergui.js b/lib/filters/filtergui.js
index f6c6dac..10629d7 100644
--- a/lib/filters/filtergui.js
+++ b/lib/filters/filtergui.js
@@ -1,40 +1,43 @@
 define([], function () {
   return function (distributor) {
-    var container = document.createElement("ul")
-    container.classList.add("filters")
-    var div = document.createElement("div")
+    var container = document.createElement("ul");
+    container.classList.add("filters");
+    var div = document.createElement("div");
 
     function render(el) {
-      el.appendChild(div)
+      el.appendChild(div);
     }
 
     function filtersChanged(filters) {
-      while (container.firstChild)
-        container.removeChild(container.firstChild)
+      while (container.firstChild) {
+        container.removeChild(container.firstChild);
+      }
 
-      filters.forEach( function (d) {
-        var li = document.createElement("li")
-        var div = document.createElement("div")
-        container.appendChild(li)
-        li.appendChild(div)
-        d.render(div)
+      filters.forEach(function (d) {
+        var li = document.createElement("li");
+        var div = document.createElement("div");
+        container.appendChild(li);
+        li.appendChild(div);
+        d.render(div);
 
-        var button = document.createElement("button")
-        button.textContent = ""
+        var button = document.createElement("button");
+        button.textContent = "";
         button.onclick = function () {
-          distributor.removeFilter(d)
-        }
-        li.appendChild(button)
-      })
+          distributor.removeFilter(d);
+        };
+        li.appendChild(button);
+      });
 
-      if (container.parentNode === div && filters.length === 0)
-        div.removeChild(container)
-      else if (filters.length > 0)
-        div.appendChild(container)
+      if (container.parentNode === div && filters.length === 0) {
+        div.removeChild(container);
+      } else if (filters.length > 0) {
+        div.appendChild(container);
+      }
     }
 
-    return { render: render,
-             filtersChanged: filtersChanged
-           }
-  }
-})
+    return {
+      render: render,
+      filtersChanged: filtersChanged
+    };
+  };
+});
diff --git a/lib/filters/genericnode.js b/lib/filters/genericnode.js
index 4c2a09d..831f575 100644
--- a/lib/filters/genericnode.js
+++ b/lib/filters/genericnode.js
@@ -1,52 +1,56 @@
 define([], function () {
   return function (name, key, value, f) {
-    var negate = false
-    var refresh
+    var negate = false;
+    var refresh;
 
-    var label = document.createElement("label")
-    var strong = document.createElement("strong")
-    label.textContent = name + " "
-    label.appendChild(strong)
+    var label = document.createElement("label");
+    var strong = document.createElement("strong");
+    label.textContent = name + " ";
+    label.appendChild(strong);
 
     function run(d) {
-      var o = dictGet(d, key.slice(0))
+      var o = dictGet(d, key.slice(0));
 
-      if (f)
-        o = f(o)
+      if (f) {
+        o = f(o);
+      }
 
-      return o === value ? !negate : negate
+      return o === value ? !negate : negate;
     }
 
     function setRefresh(f) {
-      refresh = f
+      refresh = f;
     }
 
     function draw(el) {
-      if (negate)
-        el.parentNode.classList.add("not")
-      else
-        el.parentNode.classList.remove("not")
+      if (negate) {
+        el.parentNode.classList.add("not");
+      } else {
+        el.parentNode.classList.remove("not");
+      }
 
-      strong.textContent = (negate ? "¬" : "" ) + value
+      strong.textContent = (negate ? "¬" : "" ) + value;
     }
 
     function render(el) {
-      el.appendChild(label)
-      draw(el)
+      el.appendChild(label);
+      draw(el);
 
       label.onclick = function () {
-        negate = !negate
+        negate = !negate;
 
-        draw(el)
+        draw(el);
 
-        if (refresh)
-          refresh()
-      }
+        if (refresh) {
+          refresh();
+        }
+      };
     }
 
-    return { run: run,
-             setRefresh: setRefresh,
-             render: render
-           }
-  }
-})
+    return {
+      run: run,
+      setRefresh: setRefresh,
+      render: render
+    };
+  };
+});
diff --git a/lib/filters/nodefilter.js b/lib/filters/nodefilter.js
index 17e6b41..1d6bf5f 100644
--- a/lib/filters/nodefilter.js
+++ b/lib/filters/nodefilter.js
@@ -1,33 +1,36 @@
 define([], function () {
   return function (filter) {
     return function (data) {
-      var n = Object.create(data)
-      n.nodes = {}
+      var n = Object.create(data);
+      n.nodes = {};
 
-      for (var key in data.nodes)
-        n.nodes[key] = data.nodes[key].filter(filter)
+      for (var key in data.nodes) {
+        n.nodes[key] = data.nodes[key].filter(filter);
+      }
 
-      var filteredIds = new Set()
+      var filteredIds = new Set();
 
-      n.graph = {}
-      n.graph.nodes = data.graph.nodes.filter( function (d) {
-        var r
-        if (d.node)
-          r = filter(d.node)
-        else
-          r = filter({})
+      n.graph = {};
+      n.graph.nodes = data.graph.nodes.filter(function (d) {
+        var r;
+        if (d.node) {
+          r = filter(d.node);
+        } else {
+          r = filter({});
+        }
 
-        if (r)
-          filteredIds.add(d.id)
+        if (r) {
+          filteredIds.add(d.id);
+        }
 
-        return r
-      })
+        return r;
+      });
 
-      n.graph.links = data.graph.links.filter( function (d) {
-        return filteredIds.has(d.source.id) && filteredIds.has(d.target.id)
-      })
+      n.graph.links = data.graph.links.filter(function (d) {
+        return filteredIds.has(d.source.id) && filteredIds.has(d.target.id);
+      });
 
-      return n
-    }
-  }
-})
+      return n;
+    };
+  };
+});
diff --git a/lib/forcegraph.js b/lib/forcegraph.js
index 992f1b9..b9a998c 100644
--- a/lib/forcegraph.js
+++ b/lib/forcegraph.js
@@ -1,792 +1,844 @@
 define(["d3"], function (d3) {
-  var margin = 200
-  var NODE_RADIUS = 15
-  var LINE_RADIUS = 7
+  var margin = 200;
+  var NODE_RADIUS = 15;
+  var LINE_RADIUS = 7;
 
   return function (config, linkScale, sidebar, router) {
-    var self = this
-    var canvas, ctx, screenRect
-    var nodesDict, linksDict
-    var zoomBehavior
-    var force
-    var el
-    var doAnimation = false
-    var intNodes = []
-    var intLinks = []
-    var highlight
-    var highlightedNodes = []
-    var highlightedLinks = []
-    var nodes = []
-    var uplinkNodes = []
-    var nonUplinkNodes = []
-    var unseenNodes = []
-    var unknownNodes = []
-    var savedPanZoom
+    var self = this;
+    var canvas, ctx, screenRect;
+    var nodesDict, linksDict;
+    var zoomBehavior;
+    var force;
+    var el;
+    var doAnimation = false;
+    var intNodes = [];
+    var intLinks = [];
+    var highlight;
+    var highlightedNodes = [];
+    var highlightedLinks = [];
+    var nodes = [];
+    var uplinkNodes = [];
+    var nonUplinkNodes = [];
+    var unseenNodes = [];
+    var unknownNodes = [];
+    var savedPanZoom;
 
-    var draggedNode
+    var draggedNode;
 
-    var LINK_DISTANCE = 70
+    var LINK_DISTANCE = 70;
 
     function graphDiameter(nodes) {
-      return Math.sqrt(nodes.length / Math.PI) * LINK_DISTANCE * 1.41
+      return Math.sqrt(nodes.length / Math.PI) * LINK_DISTANCE * 1.41;
     }
 
     function savePositions() {
-      if (!localStorageTest())
-        return
+      if (!localStorageTest()) {
+        return;
+      }
 
-      var save = intNodes.map( function (d) {
-        return { id: d.o.id, x: d.x, y: d.y }
-      })
+      var save = intNodes.map(function (d) {
+        return {id: d.o.id, x: d.x, y: d.y};
+      });
 
-      localStorage.setItem("graph/nodeposition", JSON.stringify(save))
+      localStorage.setItem("graph/nodeposition", JSON.stringify(save));
     }
 
     function nodeName(d) {
-      if (d.o.node && d.o.node.nodeinfo)
-        return d.o.node.nodeinfo.hostname
-      else
-        return d.o.id
+      if (d.o.node && d.o.node.nodeinfo) {
+        return d.o.node.nodeinfo.hostname;
+      } else {
+        return d.o.id;
+      }
     }
 
     function dragstart() {
-      var e = translateXY(d3.mouse(el))
+      var e = translateXY(d3.mouse(el));
 
       var nodes = intNodes.filter(function (d) {
-        return distancePoint(e, d) < NODE_RADIUS
-      })
+        return distancePoint(e, d) < NODE_RADIUS;
+      });
 
-      if (nodes.length === 0)
-        return
+      if (nodes.length === 0) {
+        return;
+      }
 
-      draggedNode = nodes[0]
-      d3.event.sourceEvent.stopPropagation()
-      d3.event.sourceEvent.preventDefault()
-      draggedNode.fixed |= 2
+      draggedNode = nodes[0];
+      d3.event.sourceEvent.stopPropagation();
+      d3.event.sourceEvent.preventDefault();
+      draggedNode.fixed |= 2;
 
-      draggedNode.px = draggedNode.x
-      draggedNode.py = draggedNode.y
+      draggedNode.px = draggedNode.x;
+      draggedNode.py = draggedNode.y;
     }
 
     function dragmove() {
       if (draggedNode) {
-        var e = translateXY(d3.mouse(el))
+        var e = translateXY(d3.mouse(el));
 
-        draggedNode.px = e.x
-        draggedNode.py = e.y
-        force.resume()
+        draggedNode.px = e.x;
+        draggedNode.py = e.y;
+        force.resume();
       }
     }
 
     function dragend() {
       if (draggedNode) {
-        d3.event.sourceEvent.stopPropagation()
-        d3.event.sourceEvent.preventDefault()
-        draggedNode.fixed &= ~2
-        draggedNode = undefined
+        d3.event.sourceEvent.stopPropagation();
+        d3.event.sourceEvent.preventDefault();
+        draggedNode.fixed &= ~2;
+        draggedNode = undefined;
       }
     }
 
     var draggableNode = d3.behavior.drag()
-                          .on("dragstart", dragstart)
-                          .on("drag", dragmove)
-                          .on("dragend", dragend)
+      .on("dragstart", dragstart)
+      .on("drag", dragmove)
+      .on("dragend", dragend);
 
     function animatePanzoom(translate, scale) {
-      var translateP = zoomBehavior.translate()
-      var scaleP = zoomBehavior.scale()
+      var translateP = zoomBehavior.translate();
+      var scaleP = zoomBehavior.scale();
 
       if (!doAnimation) {
-        zoomBehavior.translate(translate)
-        zoomBehavior.scale(scale)
-        panzoom()
+        zoomBehavior.translate(translate);
+        zoomBehavior.scale(scale);
+        panzoom();
       } else {
-        var start = {x: translateP[0], y: translateP[1], scale: scaleP}
-        var end = {x: translate[0], y: translate[1], scale: scale}
+        var start = {x: translateP[0], y: translateP[1], scale: scaleP};
+        var end = {x: translate[0], y: translate[1], scale: scale};
 
-        var interpolate = d3.interpolateObject(start, end)
-        var duration = 500
+        var interpolate = d3.interpolateObject(start, end);
+        var duration = 500;
 
-        var ease = d3.ease("cubic-in-out")
+        var ease = d3.ease("cubic-in-out");
 
         d3.timer(function (t) {
-          if (t >= duration)
-            return true
+          if (t >= duration) {
+            return true;
+          }
 
-          var v = interpolate(ease(t / duration))
-          zoomBehavior.translate([v.x, v.y])
-          zoomBehavior.scale(v.scale)
-          panzoom()
+          var v = interpolate(ease(t / duration));
+          zoomBehavior.translate([v.x, v.y]);
+          zoomBehavior.scale(v.scale);
+          panzoom();
 
-          return false
-        })
+          return false;
+        });
       }
     }
 
     function onPanZoom() {
-      savedPanZoom = {translate: zoomBehavior.translate(),
-                      scale: zoomBehavior.scale()}
-      panzoom()
+      savedPanZoom = {
+        translate: zoomBehavior.translate(),
+        scale: zoomBehavior.scale()
+      };
+      panzoom();
     }
 
     function panzoom() {
-      var translate = zoomBehavior.translate()
-      var scale = zoomBehavior.scale()
+      var translate = zoomBehavior.translate();
+      var scale = zoomBehavior.scale();
 
 
-      panzoomReal(translate, scale)
+      panzoomReal(translate, scale);
     }
 
     function panzoomReal(translate, scale) {
-      screenRect = {left: -translate[0] / scale, top: -translate[1] / scale,
-                    right: (canvas.width - translate[0]) / scale,
-                    bottom: (canvas.height - translate[1]) / scale}
+      screenRect = {
+        left: -translate[0] / scale, top: -translate[1] / scale,
+        right: (canvas.width - translate[0]) / scale,
+        bottom: (canvas.height - translate[1]) / scale
+      };
 
-      requestAnimationFrame(redraw)
+      requestAnimationFrame(redraw);
     }
 
     function getSize() {
-      var sidebarWidth = sidebar()
-      var width = el.offsetWidth - sidebarWidth
-      var height = el.offsetHeight
+      var sidebarWidth = sidebar();
+      var width = el.offsetWidth - sidebarWidth;
+      var height = el.offsetHeight;
 
-      return [width, height]
+      return [width, height];
     }
 
     function panzoomTo(a, b) {
-      var sidebarWidth = sidebar()
-      var size = getSize()
+      var sidebarWidth = sidebar();
+      var size = getSize();
 
-      var targetWidth = Math.max(1, b[0] - a[0])
-      var targetHeight = Math.max(1, b[1] - a[1])
+      var targetWidth = Math.max(1, b[0] - a[0]);
+      var targetHeight = Math.max(1, b[1] - a[1]);
 
-      var scaleX = size[0] / targetWidth
-      var scaleY = size[1] / targetHeight
-      var scaleMax = zoomBehavior.scaleExtent()[1]
-      var scale = 0.5 * Math.min(scaleMax, Math.min(scaleX, scaleY))
+      var scaleX = size[0] / targetWidth;
+      var scaleY = size[1] / targetHeight;
+      var scaleMax = zoomBehavior.scaleExtent()[1];
+      var scale = 0.5 * Math.min(scaleMax, Math.min(scaleX, scaleY));
 
-      var centroid = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]
-      var x = -centroid[0] * scale + size[0] / 2
-      var y = -centroid[1] * scale + size[1] / 2
-      var translate = [x + sidebarWidth, y]
+      var centroid = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
+      var x = -centroid[0] * scale + size[0] / 2;
+      var y = -centroid[1] * scale + size[1] / 2;
+      var translate = [x + sidebarWidth, y];
 
-      animatePanzoom(translate, scale)
+      animatePanzoom(translate, scale);
     }
 
     function updateHighlight(nopanzoom) {
-      highlightedNodes = []
-      highlightedLinks = []
+      highlightedNodes = [];
+      highlightedLinks = [];
 
-      if (highlight !== undefined)
+      if (highlight !== undefined) {
         if (highlight.type === "node") {
-          var n = nodesDict[highlight.o.nodeinfo.node_id]
+          var n = nodesDict[highlight.o.nodeinfo.node_id];
 
           if (n) {
-            highlightedNodes = [n]
-
-            if (!nopanzoom)
-              panzoomTo([n.x, n.y], [n.x, n.y])
-          }
-
-          return
-        } else if (highlight.type === "link") {
-          var l = linksDict[highlight.o.id]
-
-          if (l) {
-            highlightedLinks = [l]
+            highlightedNodes = [n];
 
             if (!nopanzoom) {
-              var x = d3.extent([l.source, l.target], function (d) { return d.x })
-              var y = d3.extent([l.source, l.target], function (d) { return d.y })
-              panzoomTo([x[0], y[0]], [x[1], y[1]])
+              panzoomTo([n.x, n.y], [n.x, n.y]);
             }
           }
 
-          return
-        }
+          return;
+        } else if (highlight.type === "link") {
+          var l = linksDict[highlight.o.id];
 
-      if (!nopanzoom)
-        if (!savedPanZoom)
-          panzoomTo([0, 0], force.size())
-        else
-          animatePanzoom(savedPanZoom.translate, savedPanZoom.scale)
+          if (l) {
+            highlightedLinks = [l];
+
+            if (!nopanzoom) {
+              var x = d3.extent([l.source, l.target], function (d) {
+                return d.x;
+              });
+              var y = d3.extent([l.source, l.target], function (d) {
+                return d.y;
+              });
+              panzoomTo([x[0], y[0]], [x[1], y[1]]);
+            }
+          }
+
+          return;
+        }
+      }
+
+      if (!nopanzoom) {
+        if (!savedPanZoom) {
+          panzoomTo([0, 0], force.size());
+        } else {
+          animatePanzoom(savedPanZoom.translate, savedPanZoom.scale);
+        }
+      }
     }
 
     function drawLabel(d) {
       var neighbours = d.neighbours.filter(function (d) {
-        return !d.link.o.isVPN
-      })
+        return !d.link.o.isVPN;
+      });
 
       var sum = neighbours.reduce(function (a, b) {
-        return [a[0] + b.node.x, a[1] + b.node.y]
-      }, [0, 0])
+        return [a[0] + b.node.x, a[1] + b.node.y];
+      }, [0, 0]);
 
-      var sumCos = sum[0] - d.x * neighbours.length
-      var sumSin = sum[1] - d.y * neighbours.length
+      var sumCos = sum[0] - d.x * neighbours.length;
+      var sumSin = sum[1] - d.y * neighbours.length;
 
-      var angle = Math.PI / 2
+      var angle = Math.PI / 2;
 
-      if (neighbours.length > 0)
-        angle = Math.PI + Math.atan2(sumSin, sumCos)
+      if (neighbours.length > 0) {
+        angle = Math.PI + Math.atan2(sumSin, sumCos);
+      }
 
-      var cos = Math.cos(angle)
-      var sin = Math.sin(angle)
+      var cos = Math.cos(angle);
+      var sin = Math.sin(angle);
 
-      var width = d.labelWidth
-      var height = d.labelHeight
+      var width = d.labelWidth;
+      var height = d.labelHeight;
 
-      var x = d.x + d.labelA * Math.pow(Math.abs(cos), 2 / 5) * Math.sign(cos) - width / 2
-      var y = d.y + d.labelB * Math.pow(Math.abs(sin), 2 / 5) * Math.sign(sin) - height / 2
+      var x = d.x + d.labelA * Math.pow(Math.abs(cos), 2 / 5) * Math.sign(cos) - width / 2;
+      var y = d.y + d.labelB * Math.pow(Math.abs(sin), 2 / 5) * Math.sign(sin) - height / 2;
 
-      ctx.drawImage(d.label, x, y, width, height)
+      ctx.drawImage(d.label, x, y, width, height);
     }
 
     function visibleLinks(d) {
       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.target.x > screenRect.left && d.target.x < screenRect.right &&
-              d.target.y > screenRect.top && d.target.y < screenRect.bottom)
+        d.source.x > screenRect.left && d.source.x < screenRect.right &&
+        d.source.y > screenRect.top && d.source.y < screenRect.bottom) ||
+        (d.target.x > screenRect.left && d.target.x < screenRect.right &&
+        d.target.y > screenRect.top && d.target.y < screenRect.bottom);
     }
 
     function visibleNodes(d) {
       return d.x + margin > screenRect.left && d.x - margin < screenRect.right &&
-             d.y + margin > screenRect.top && d.y - margin < screenRect.bottom
+        d.y + margin > screenRect.top && d.y - margin < screenRect.bottom;
     }
 
     function drawNode(color, radius, scale, r) {
-      var node = document.createElement("canvas")
-      node.width = scale * radius * 8 * r
-      node.height = node.width
+      var node = document.createElement("canvas");
+      node.width = scale * radius * 8 * r;
+      node.height = node.width;
 
-      var nctx = node.getContext("2d")
-      nctx.scale(scale * r, scale * r)
-      nctx.save()
+      var nctx = node.getContext("2d");
+      nctx.scale(scale * r, scale * r);
+      nctx.save();
 
-      nctx.translate(-node.width / scale, -node.height / scale)
-      nctx.lineWidth = radius
+      nctx.translate(-node.width / scale, -node.height / scale);
+      nctx.lineWidth = radius;
 
-      nctx.beginPath()
-      nctx.moveTo(radius, 0)
-      nctx.arc(0, 0, radius, 0, 2 * Math.PI)
+      nctx.beginPath();
+      nctx.moveTo(radius, 0);
+      nctx.arc(0, 0, radius, 0, 2 * Math.PI);
 
-      nctx.strokeStyle = "rgba(255, 0, 0, 1)"
-      nctx.shadowOffsetX = node.width * 1.5 + 0
-      nctx.shadowOffsetY = node.height * 1.5 + 3
-      nctx.shadowBlur = 12
-      nctx.shadowColor = "rgba(0, 0, 0, 0.16)"
-      nctx.stroke()
-      nctx.shadowOffsetX = node.width * 1.5 + 0
-      nctx.shadowOffsetY = node.height * 1.5 + 3
-      nctx.shadowBlur = 12
-      nctx.shadowColor = "rgba(0, 0, 0, 0.23)"
-      nctx.stroke()
+      nctx.strokeStyle = "rgba(255, 0, 0, 1)";
+      nctx.shadowOffsetX = node.width * 1.5 + 0;
+      nctx.shadowOffsetY = node.height * 1.5 + 3;
+      nctx.shadowBlur = 12;
+      nctx.shadowColor = "rgba(0, 0, 0, 0.16)";
+      nctx.stroke();
+      nctx.shadowOffsetX = node.width * 1.5 + 0;
+      nctx.shadowOffsetY = node.height * 1.5 + 3;
+      nctx.shadowBlur = 12;
+      nctx.shadowColor = "rgba(0, 0, 0, 0.23)";
+      nctx.stroke();
 
-      nctx.restore()
-      nctx.translate(node.width / 2 / scale / r, node.height / 2 / scale / r)
+      nctx.restore();
+      nctx.translate(node.width / 2 / scale / r, node.height / 2 / scale / r);
 
-      nctx.beginPath()
-      nctx.moveTo(radius, 0)
-      nctx.arc(0, 0, radius, 0, 2 * Math.PI)
+      nctx.beginPath();
+      nctx.moveTo(radius, 0);
+      nctx.arc(0, 0, radius, 0, 2 * Math.PI);
 
-      nctx.strokeStyle = color
-      nctx.lineWidth = radius
-      nctx.stroke()
+      nctx.strokeStyle = color;
+      nctx.lineWidth = radius;
+      nctx.stroke();
 
-      return node
+      return node;
     }
 
     function redraw() {
-      var r = window.devicePixelRatio
-      var translate = zoomBehavior.translate()
-      var scale = zoomBehavior.scale()
-      var links = intLinks.filter(visibleLinks)
+      var r = window.devicePixelRatio;
+      var translate = zoomBehavior.translate();
+      var scale = zoomBehavior.scale();
+      var links = intLinks.filter(visibleLinks);
 
-      ctx.save()
-      ctx.setTransform(1, 0, 0, 1, 0, 0)
-      ctx.clearRect(0, 0, canvas.width, canvas.height)
-      ctx.restore()
+      ctx.save();
+      ctx.setTransform(1, 0, 0, 1, 0, 0);
+      ctx.clearRect(0, 0, canvas.width, canvas.height);
+      ctx.restore();
 
-      ctx.save()
-      ctx.translate(translate[0], translate[1])
-      ctx.scale(scale, scale)
+      ctx.save();
+      ctx.translate(translate[0], translate[1]);
+      ctx.scale(scale, scale);
 
-      var clientColor = "rgba(230, 50, 75, 1.0)"
-      var unknownColor = "#D10E2A"
-      var nonUplinkColor = "#F2E3C6"
-      var uplinkColor = "#5BAAEB"
-      var unseenColor = "#FFA726"
-      var highlightColor = "rgba(252, 227, 198, 0.15)"
-      var nodeRadius = 6
-      var cableColor = "#50B0F0"
+      var clientColor = "rgba(230, 50, 75, 1.0)";
+      var unknownColor = "#D10E2A";
+      var nonUplinkColor = "#F2E3C6";
+      var uplinkColor = "#5BAAEB";
+      var unseenColor = "#FFA726";
+      var highlightColor = "rgba(252, 227, 198, 0.15)";
+      var nodeRadius = 6;
+      var cableColor = "#50B0F0";
 
       // -- draw links --
-      ctx.save()
+      ctx.save();
       links.forEach(function (d) {
-        var dx = d.target.x - d.source.x
-        var dy = d.target.y - d.source.y
-        var a = Math.sqrt(dx * dx + dy * dy) * 2
-        dx /= a
-        dy /= a
+        var dx = d.target.x - d.source.x;
+        var dy = d.target.y - d.source.y;
+        var a = Math.sqrt(dx * dx + dy * dy) * 2;
+        dx /= a;
+        dy /= a;
 
-        var distancex = d.target.x - d.source.x - (10 * dx)
-        var distancey = d.target.y - d.source.y - (10 * dy)
+        var distancex = d.target.x - d.source.x - (10 * dx);
+        var distancey = d.target.y - d.source.y - (10 * dy);
 
-        ctx.beginPath()
-        ctx.moveTo(d.source.x + dx * nodeRadius, d.source.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.globalAlpha = d.o.isVPN ? 0.1 : 0.8
-        ctx.lineWidth = d.o.isVPN ? 1.5 : 2.5
-        ctx.stroke()
-      })
+        ctx.beginPath();
+        ctx.moveTo(d.source.x + dx * nodeRadius, d.source.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.globalAlpha = d.o.isVPN ? 0.1 : 0.8;
+        ctx.lineWidth = d.o.isVPN ? 1.5 : 2.5;
+        ctx.stroke();
+      });
 
-      ctx.restore()
+      ctx.restore();
 
       // -- draw unknown nodes --
-      ctx.beginPath()
+      ctx.beginPath();
       unknownNodes.filter(visibleNodes).forEach(function (d) {
-        ctx.moveTo(d.x + nodeRadius, d.y)
-        ctx.arc(d.x, d.y, nodeRadius, 0, 2 * Math.PI)
-      })
+        ctx.moveTo(d.x + nodeRadius, d.y);
+        ctx.arc(d.x, d.y, nodeRadius, 0, 2 * Math.PI);
+      });
 
-      ctx.strokeStyle = unknownColor
-      ctx.lineWidth = nodeRadius
+      ctx.strokeStyle = unknownColor;
+      ctx.lineWidth = nodeRadius;
 
-      ctx.stroke()
+      ctx.stroke();
 
 
       // -- draw nodes --
-      ctx.save()
-      ctx.scale(1 / scale / r, 1 / scale / r)
+      ctx.save();
+      ctx.scale(1 / scale / r, 1 / scale / r);
 
-      var nonUplinkNode = drawNode(nonUplinkColor, nodeRadius, scale, r)
+      var nonUplinkNode = drawNode(nonUplinkColor, nodeRadius, scale, r);
       nonUplinkNodes.filter(visibleNodes).forEach(function (d) {
-        ctx.drawImage(nonUplinkNode, scale * r * d.x - nonUplinkNode.width / 2, scale * r * d.y - nonUplinkNode.height / 2)
-      })
+        ctx.drawImage(nonUplinkNode, scale * r * d.x - nonUplinkNode.width / 2, scale * r * d.y - nonUplinkNode.height / 2);
+      });
 
-      var uplinkNode = drawNode(uplinkColor, nodeRadius, scale, r)
+      var uplinkNode = drawNode(uplinkColor, nodeRadius, scale, r);
       uplinkNodes.filter(visibleNodes).forEach(function (d) {
-        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)
+      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.drawImage(unseenNode, scale * r * d.x - unseenNode.width / 2, scale * r * d.y - unseenNode.height / 2);
+      });
 
-      ctx.restore()
+      ctx.restore();
 
       // -- draw clients --
-      ctx.save()
-      ctx.beginPath()
-      if (scale > 0.9)
+      ctx.save();
+      ctx.beginPath();
+      if (scale > 0.9) {
         nodes.filter(visibleNodes).forEach(function (d) {
-          var clients = d.o.node.statistics.clients
-          if (clients === 0)
-            return
+          var clients = d.o.node.statistics.clients;
+          if (clients === 0) {
+            return;
+          }
 
-          var startDistance = 16
-          var radius = 3
-          var a = 1.2
-          var startAngle = Math.PI
+          var startDistance = 16;
+          var radius = 3;
+          var a = 1.2;
+          var startAngle = Math.PI;
 
           for (var orbit = 0, i = 0; i < clients; orbit++) {
-            var distance = startDistance + orbit * 2 * radius * a
-            var n = Math.floor((Math.PI * distance) / (a * radius))
-            var delta = clients - i
+            var distance = startDistance + orbit * 2 * radius * a;
+            var n = Math.floor((Math.PI * distance) / (a * radius));
+            var delta = clients - i;
 
             for (var j = 0; j < Math.min(delta, n); i++, j++) {
-              var angle = 2 * Math.PI / n * j
-              var x = d.x + distance * Math.cos(angle + startAngle)
-              var y = d.y + distance * Math.sin(angle + startAngle)
+              var angle = 2 * Math.PI / n * j;
+              var x = d.x + distance * Math.cos(angle + startAngle);
+              var y = d.y + distance * Math.sin(angle + startAngle);
 
-              ctx.moveTo(x, y)
-              ctx.arc(x, y, radius, 0, 2 * Math.PI)
+              ctx.moveTo(x, y);
+              ctx.arc(x, y, radius, 0, 2 * Math.PI);
             }
           }
-        })
+        });
+      }
 
-      ctx.fillStyle = clientColor
-      ctx.fill()
-      ctx.restore()
+      ctx.fillStyle = clientColor;
+      ctx.fill();
+      ctx.restore();
 
       // -- draw node highlights --
       if (highlightedNodes.length) {
-        ctx.save()
-        ctx.shadowColor = "rgba(255, 255, 255, 1.0)"
-        ctx.shadowBlur = 10 * nodeRadius
-        ctx.shadowOffsetX = 0
-        ctx.shadowOffsetY = 0
-        ctx.globalCompositeOperation = "lighten"
-        ctx.fillStyle = highlightColor
+        ctx.save();
+        ctx.shadowColor = "rgba(255, 255, 255, 1.0)";
+        ctx.shadowBlur = 10 * nodeRadius;
+        ctx.shadowOffsetX = 0;
+        ctx.shadowOffsetY = 0;
+        ctx.globalCompositeOperation = "lighten";
+        ctx.fillStyle = highlightColor;
 
-        ctx.beginPath()
+        ctx.beginPath();
         highlightedNodes.forEach(function (d) {
-          ctx.moveTo(d.x + 5 * nodeRadius, d.y)
-          ctx.arc(d.x, d.y, 5 * nodeRadius, 0, 2 * Math.PI)
-        })
-        ctx.fill()
+          ctx.moveTo(d.x + 5 * nodeRadius, d.y);
+          ctx.arc(d.x, d.y, 5 * nodeRadius, 0, 2 * Math.PI);
+        });
+        ctx.fill();
 
-        ctx.restore()
+        ctx.restore();
       }
 
       // -- draw link highlights --
       if (highlightedLinks.length) {
-        ctx.save()
-        ctx.lineWidth = 2 * 5 * nodeRadius
-        ctx.shadowColor = "rgba(255, 255, 255, 1.0)"
-        ctx.shadowBlur = 10 * nodeRadius
-        ctx.shadowOffsetX = 0
-        ctx.shadowOffsetY = 0
-        ctx.globalCompositeOperation = "lighten"
-        ctx.strokeStyle = highlightColor
-        ctx.lineCap = "round"
+        ctx.save();
+        ctx.lineWidth = 2 * 5 * nodeRadius;
+        ctx.shadowColor = "rgba(255, 255, 255, 1.0)";
+        ctx.shadowBlur = 10 * nodeRadius;
+        ctx.shadowOffsetX = 0;
+        ctx.shadowOffsetY = 0;
+        ctx.globalCompositeOperation = "lighten";
+        ctx.strokeStyle = highlightColor;
+        ctx.lineCap = "round";
 
-        ctx.beginPath()
+        ctx.beginPath();
         highlightedLinks.forEach(function (d) {
-          ctx.moveTo(d.source.x, d.source.y)
-          ctx.lineTo(d.target.x, d.target.y)
-        })
-        ctx.stroke()
+          ctx.moveTo(d.source.x, d.source.y);
+          ctx.lineTo(d.target.x, d.target.y);
+        });
+        ctx.stroke();
 
-        ctx.restore()
+        ctx.restore();
       }
 
       // -- draw labels --
-      if (scale > 0.9)
-        intNodes.filter(visibleNodes).forEach(drawLabel, scale)
+      if (scale > 0.9) {
+        intNodes.filter(visibleNodes).forEach(drawLabel, scale);
+      }
 
-      ctx.restore()
+      ctx.restore();
     }
 
     function tickEvent() {
-      redraw()
+      redraw();
     }
 
     function resizeCanvas() {
-      var r = window.devicePixelRatio
-      canvas.width = el.offsetWidth * r
-      canvas.height = el.offsetHeight * r
-      canvas.style.width = el.offsetWidth + "px"
-      canvas.style.height = el.offsetHeight + "px"
-      ctx.setTransform(1, 0, 0, 1, 0, 0)
-      ctx.scale(r, r)
-      requestAnimationFrame(redraw)
+      var r = window.devicePixelRatio;
+      canvas.width = el.offsetWidth * r;
+      canvas.height = el.offsetHeight * r;
+      canvas.style.width = el.offsetWidth + "px";
+      canvas.style.height = el.offsetHeight + "px";
+      ctx.setTransform(1, 0, 0, 1, 0, 0);
+      ctx.scale(r, r);
+      requestAnimationFrame(redraw);
     }
 
     function distance(ax, ay, bx, by) {
-      return Math.pow(ax - bx, 2) + Math.pow(ay - by, 2)
+      return Math.pow(ax - bx, 2) + Math.pow(ay - by, 2);
     }
 
     function distancePoint(a, b) {
-      return Math.sqrt(distance(a.x, a.y, b.x, b.y))
+      return Math.sqrt(distance(a.x, a.y, b.x, b.y));
     }
 
     function distanceLink(p, a, b) {
       /* http://stackoverflow.com/questions/849211 */
 
-      var bx = b.x - ((b.x - a.x) / 2)
-      var by = b.y - ((b.y - a.y) / 2)
+      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)
+      var l2 = distance(a.x, a.y, bx, by);
 
-      if (l2 === 0)
-        return distance(p.x, p.y, a.x, a.y)
+      if (l2 === 0) {
+        return distance(p.x, p.y, a.x, a.y);
+      }
 
-      var t = ((p.x - a.x) * (bx - a.x) + (p.y - a.y) * (by - a.y)) / l2
+      var t = ((p.x - a.x) * (bx - a.x) + (p.y - a.y) * (by - a.y)) / l2;
 
-      if (t < 0)
-        return distance(p.x, p.y, a.x, a.y)
+      if (t < 0) {
+        return distance(p.x, p.y, a.x, a.y);
+      }
 
-      if (t > 1)
-        return distance(p.x, p.y, bx, by)
+      if (t > 1) {
+        return distance(p.x, p.y, bx, by);
+      }
 
-      return Math.sqrt(distance(p.x, p.y, a.x + t * (bx - a.x), a.y + t * (by - a.y) ))
+      return Math.sqrt(distance(p.x, p.y, a.x + t * (bx - a.x), a.y + t * (by - a.y)));
     }
 
     function translateXY(d) {
-      var translate = zoomBehavior.translate()
-      var scale = zoomBehavior.scale()
+      var translate = zoomBehavior.translate();
+      var scale = zoomBehavior.scale();
 
-      return {x: (d[0] - translate[0]) / scale,
-              y: (d[1] - translate[1]) / scale
-             }
+      return {
+        x: (d[0] - translate[0]) / scale,
+        y: (d[1] - translate[1]) / scale
+      };
     }
 
     function onClick() {
-      if (d3.event.defaultPrevented)
-        return
+      if (d3.event.defaultPrevented) {
+        return;
+      }
 
-      var e = translateXY(d3.mouse(el))
+      var e = translateXY(d3.mouse(el));
 
       var nodes = intNodes.filter(function (d) {
-        return distancePoint(e, d) < NODE_RADIUS
-      })
+        return distancePoint(e, d) < NODE_RADIUS;
+      });
 
       if (nodes.length > 0) {
-        router.node(nodes[0].o.node)()
-        return
+        router.node(nodes[0].o.node)();
+        return;
       }
 
       var links = intLinks.filter(function (d) {
-        return !d.o.isVPN
+        return !d.o.isVPN;
       }).filter(function (d) {
-        return distanceLink(e, d.source, d.target) < LINE_RADIUS
-      })
+        return distanceLink(e, d.source, d.target) < LINE_RADIUS;
+      });
 
       if (links.length > 0) {
-        router.link(links[0].o)()
-        return
+        router.link(links[0].o)();
+
       }
     }
 
     function zoom(z, scale) {
-      var size = getSize()
-      var newSize = [size[0] / scale, size[1] / scale]
+      var size = getSize();
+      var newSize = [size[0] / scale, size[1] / scale];
 
-      var sidebarWidth = sidebar()
-      var delta = [size[0] - newSize[0], size[1] - newSize[1]]
-      var translate = z.translate()
-      var translateNew = [sidebarWidth + (translate[0] - sidebarWidth - delta[0] / 2) * scale, (translate[1] - delta[1] / 2) * scale]
+      var sidebarWidth = sidebar();
+      var delta = [size[0] - newSize[0], size[1] - newSize[1]];
+      var translate = z.translate();
+      var translateNew = [sidebarWidth + (translate[0] - sidebarWidth - delta[0] / 2) * scale, (translate[1] - delta[1] / 2) * scale];
 
-      animatePanzoom(translateNew, z.scale() * scale)
+      animatePanzoom(translateNew, z.scale() * scale);
     }
 
     function keyboardZoom(z) {
       return function () {
-        var e = d3.event
+        var e = d3.event;
 
-        if (e.altKey || e.ctrlKey || e.metaKey)
-          return
+        if (e.altKey || e.ctrlKey || e.metaKey) {
+          return;
+        }
 
-        if (e.keyCode === 43)
-          zoom(z, 1.41)
+        if (e.keyCode === 43) {
+          zoom(z, 1.41);
+        }
 
-        if (e.keyCode === 45)
-          zoom(z, 1 / 1.41)
-      }
+        if (e.keyCode === 45) {
+          zoom(z, 1 / 1.41);
+        }
+      };
     }
 
-    el = document.createElement("div")
-    el.classList.add("graph")
+    el = document.createElement("div");
+    el.classList.add("graph");
 
     zoomBehavior = d3.behavior.zoom()
-                     .scaleExtent([1 / 3, 3])
-                     .on("zoom", onPanZoom)
-                     .translate([sidebar(), 0])
+      .scaleExtent([1 / 3, 3])
+      .on("zoom", onPanZoom)
+      .translate([sidebar(), 0]);
 
     canvas = d3.select(el)
-               .attr("tabindex", 1)
-               .on("keypress", keyboardZoom(zoomBehavior))
-               .call(zoomBehavior)
-               .append("canvas")
-               .on("click", onClick)
-               .call(draggableNode)
-               .node()
+      .attr("tabindex", 1)
+      .on("keypress", keyboardZoom(zoomBehavior))
+      .call(zoomBehavior)
+      .append("canvas")
+      .on("click", onClick)
+      .call(draggableNode)
+      .node();
 
-    ctx = canvas.getContext("2d")
+    ctx = canvas.getContext("2d");
 
     force = d3.layout.force()
-              .charge(-250)
-              .gravity(0.1)
-              .linkDistance(function (d) {
-                if (d.o.isVPN)
-                  return 0
-                else
-                  return LINK_DISTANCE
-              })
-              .linkStrength(function (d) {
-                if (d.o.isVPN)
-                  return 0
-                else
-                  return Math.max(0.5, 1 / d.o.tq)
-              })
-              .on("tick", tickEvent)
-              .on("end", savePositions)
+      .charge(-250)
+      .gravity(0.1)
+      .linkDistance(function (d) {
+        if (d.o.isVPN) {
+          return 0;
+        } else {
+          return LINK_DISTANCE;
+        }
+      })
+      .linkStrength(function (d) {
+        if (d.o.isVPN) {
+          return 0;
+        } else {
+          return Math.max(0.5, 1 / d.o.tq);
+        }
+      })
+      .on("tick", tickEvent)
+      .on("end", savePositions);
 
-    window.addEventListener("resize", resizeCanvas)
+    window.addEventListener("resize", resizeCanvas);
 
-    panzoom()
+    panzoom();
 
     self.setData = function (data) {
-      var oldNodes = {}
-
-      intNodes.forEach( function (d) {
-        oldNodes[d.o.id] = d
-      })
-
-      intNodes = data.graph.nodes.map( function (d) {
-        var e
-        if (d.id in oldNodes)
-          e = oldNodes[d.id]
-        else
-          e = {}
-
-        e.o = d
-
-        return e
-      })
-
-      var newNodesDict = {}
-
-      intNodes.forEach( function (d) {
-        newNodesDict[d.o.id] = d
-      })
-
-      var oldLinks = {}
-
-      intLinks.forEach( function (d) {
-        oldLinks[d.o.id] = d
-      })
-
-      intLinks = data.graph.links.map( function (d) {
-        var e
-        if (d.id in oldLinks)
-          e = oldLinks[d.id]
-        else
-          e = {}
-
-        e.o = d
-        e.source = newNodesDict[d.source.id]
-        e.target = newNodesDict[d.target.id]
-
-        if (d.isVPN)
-          e.color = "rgba(255, 255, 255, " + (0.6 / d.tq) + ")"
-        else
-          e.color = linkScale(d.tq).hex()
-
-        return e
-      })
-
-      linksDict = {}
-      nodesDict = {}
+      var oldNodes = {};
 
       intNodes.forEach(function (d) {
-        d.neighbours = {}
+        oldNodes[d.o.id] = d;
+      });
 
-        if (d.o.node)
-          nodesDict[d.o.node.nodeinfo.node_id] = d
+      intNodes = data.graph.nodes.map(function (d) {
+        var e;
+        if (d.id in oldNodes) {
+          e = oldNodes[d.id];
+        } else {
+          e = {};
+        }
 
-        var name = nodeName(d)
+        e.o = d;
 
-        var offset = 5
-        var lineWidth = 3
-        var buffer = document.createElement("canvas")
-        var r = window.devicePixelRatio
-        var bctx = buffer.getContext("2d")
-        bctx.font = "11px Roboto"
-        var width = bctx.measureText(name).width
-        var scale = zoomBehavior.scaleExtent()[1] * r
-        buffer.width = (width + 2 * lineWidth) * scale
-        buffer.height = (16 + 2 * lineWidth) * scale
-        bctx.scale(scale, scale)
-        bctx.textBaseline = "middle"
-        bctx.textAlign = "center"
-        bctx.fillStyle = "rgba(242, 227, 198, 1.0)"
-        bctx.shadowColor = "rgba(0, 0, 0, 1)"
-        bctx.shadowBlur = 5
-        bctx.fillText(name, buffer.width / (2 * scale), buffer.height / (2 * scale))
+        return e;
+      });
 
-        d.label = buffer
-        d.labelWidth = buffer.width / scale
-        d.labelHeight = buffer.height / scale
-        d.labelA = offset + buffer.width / (2 * scale)
-        d.labelB = offset + buffer.height / (2 * scale)
-      })
+      var newNodesDict = {};
+
+      intNodes.forEach(function (d) {
+        newNodesDict[d.o.id] = d;
+      });
+
+      var oldLinks = {};
 
       intLinks.forEach(function (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}
+        oldLinks[d.o.id] = d;
+      });
 
-        if (d.o.source && d.o.target)
-          linksDict[d.o.id] = d
-      })
+      intLinks = data.graph.links.map(function (d) {
+        var e;
+        if (d.id in oldLinks) {
+          e = oldLinks[d.id];
+        } else {
+          e = {};
+        }
+
+        e.o = d;
+        e.source = newNodesDict[d.source.id];
+        e.target = newNodesDict[d.target.id];
+
+        if (d.isVPN) {
+          e.color = "rgba(255, 255, 255, " + (0.6 / d.tq) + ")";
+        } else {
+          e.color = linkScale(d.tq).hex();
+        }
+
+        return e;
+      });
+
+      linksDict = {};
+      nodesDict = {};
+
+      intNodes.forEach(function (d) {
+        d.neighbours = {};
+
+        if (d.o.node) {
+          nodesDict[d.o.node.nodeinfo.node_id] = d;
+        }
+
+        var name = nodeName(d);
+
+        var offset = 5;
+        var lineWidth = 3;
+        var buffer = document.createElement("canvas");
+        var r = window.devicePixelRatio;
+        var bctx = buffer.getContext("2d");
+        bctx.font = "11px Roboto";
+        var width = bctx.measureText(name).width;
+        var scale = zoomBehavior.scaleExtent()[1] * r;
+        buffer.width = (width + 2 * lineWidth) * scale;
+        buffer.height = (16 + 2 * lineWidth) * scale;
+        bctx.scale(scale, scale);
+        bctx.textBaseline = "middle";
+        bctx.textAlign = "center";
+        bctx.fillStyle = "rgba(242, 227, 198, 1.0)";
+        bctx.shadowColor = "rgba(0, 0, 0, 1)";
+        bctx.shadowBlur = 5;
+        bctx.fillText(name, buffer.width / (2 * scale), buffer.height / (2 * scale));
+
+        d.label = buffer;
+        d.labelWidth = buffer.width / scale;
+        d.labelHeight = buffer.height / scale;
+        d.labelA = offset + buffer.width / (2 * scale);
+        d.labelB = offset + buffer.height / (2 * scale);
+      });
 
       intLinks.forEach(function (d) {
-        if (linksDict[d.target.o.node_id + "-" + d.source.o.node_id])
-          return
+        d.source.neighbours[d.target.o.id] = {node: d.target, link: d};
+        d.target.neighbours[d.source.o.id] = {node: d.source, link: d};
 
-        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
-      })
+        if (d.o.source && d.o.target) {
+          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) {
         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.unseen && d.o.node })
-      uplinkNodes = 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 })
+      nodes = intNodes.filter(function (d) {
+        return !d.o.unseen && d.o.node;
+      });
+      uplinkNodes = 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;
+      });
 
       if (localStorageTest()) {
-        var save = JSON.parse(localStorage.getItem("graph/nodeposition"))
+        var save = JSON.parse(localStorage.getItem("graph/nodeposition"));
 
         if (save) {
-          var nodePositions = {}
-          save.forEach( function (d) {
-            nodePositions[d.id] = d
-          })
+          var nodePositions = {};
+          save.forEach(function (d) {
+            nodePositions[d.id] = d;
+          });
 
-          intNodes.forEach( function (d) {
+          intNodes.forEach(function (d) {
             if (nodePositions[d.o.id] && (d.x === undefined || d.y === undefined)) {
-              d.x = nodePositions[d.o.id].x
-              d.y = nodePositions[d.o.id].y
+              d.x = nodePositions[d.o.id].x;
+              d.y = nodePositions[d.o.id].y;
             }
-          })
+          });
         }
       }
 
-      var diameter = graphDiameter(intNodes)
+      var diameter = graphDiameter(intNodes);
 
       force.nodes(intNodes)
-           .links(intLinks)
-           .size([diameter, diameter])
+        .links(intLinks)
+        .size([diameter, diameter]);
 
-      updateHighlight(true)
+      updateHighlight(true);
 
-      force.start()
-      resizeCanvas()
-    }
+      force.start();
+      resizeCanvas();
+    };
 
     self.resetView = function () {
-      highlight = undefined
-      updateHighlight()
-      doAnimation = true
-    }
+      highlight = undefined;
+      updateHighlight();
+      doAnimation = true;
+    };
 
     self.gotoNode = function (d) {
-      highlight = {type: "node", o: d}
-      updateHighlight()
-      doAnimation = true
-    }
+      highlight = {type: "node", o: d};
+      updateHighlight();
+      doAnimation = true;
+    };
 
     self.gotoLink = function (d) {
-      highlight = {type: "link", o: d}
-      updateHighlight()
-      doAnimation = true
-    }
+      highlight = {type: "link", o: d};
+      updateHighlight();
+      doAnimation = true;
+    };
 
     self.destroy = function () {
-      force.stop()
-      canvas.remove()
-      force = null
+      force.stop();
+      canvas.remove();
+      force = null;
 
-      if (el.parentNode)
-        el.parentNode.removeChild(el)
-    }
+      if (el.parentNode) {
+        el.parentNode.removeChild(el);
+      }
+    };
 
     self.render = function (d) {
-      d.appendChild(el)
-      resizeCanvas()
-      updateHighlight()
-    }
+      d.appendChild(el);
+      resizeCanvas();
+      updateHighlight();
+    };
 
-    return self
-  }
-})
+    return self;
+  };
+});
diff --git a/lib/gui.js b/lib/gui.js
index 4e14c50..f757e29 100644
--- a/lib/gui.js
+++ b/lib/gui.js
@@ -1,125 +1,127 @@
-define([ "chroma-js", "map", "sidebar", "tabs", "container", "meshstats",
-         "legend", "linklist", "nodelist", "simplenodelist", "infobox/main",
-         "proportions", "forcegraph", "title", "about", "datadistributor",
-         "filters/filtergui" ],
-function (chroma, Map, Sidebar, Tabs, Container, Meshstats, Legend, Linklist,
-          Nodelist, SimpleNodelist, Infobox, Proportions, ForceGraph,
-          Title, About, DataDistributor, FilterGUI) {
-  return function (config, router) {
-    var self = this
-    var content
-    var contentDiv
+define(["chroma-js", "map", "sidebar", "tabs", "container", "meshstats",
+    "legend", "linklist", "nodelist", "simplenodelist", "infobox/main",
+    "proportions", "forcegraph", "title", "about", "datadistributor",
+    "filters/filtergui"],
+  function (chroma, Map, Sidebar, Tabs, Container, Meshstats, Legend, Linklist,
+            Nodelist, SimpleNodelist, Infobox, Proportions, ForceGraph,
+            Title, About, DataDistributor, FilterGUI) {
+    return function (config, router) {
+      var self = this;
+      var content;
+      var contentDiv;
 
-    var linkScale = chroma.scale(chroma.interpolate.bezier(["#04C714", "#FF5500", "#F02311"])).domain([1, 5])
-    var sidebar
+      var linkScale = chroma.scale(chroma.interpolate.bezier(["#04C714", "#FF5500", "#F02311"])).domain([1, 5]);
+      var sidebar;
 
-    var buttons = document.createElement("div")
-    buttons.classList.add("buttons")
+      var buttons = document.createElement("div");
+      buttons.classList.add("buttons");
 
-    var fanout = new DataDistributor()
-    var fanoutUnfiltered = new DataDistributor()
-    fanoutUnfiltered.add(fanout)
+      var fanout = new DataDistributor();
+      var fanoutUnfiltered = new DataDistributor();
+      fanoutUnfiltered.add(fanout);
 
-    function removeContent() {
-      if (!content)
-        return
+      function removeContent() {
+        if (!content) {
+          return;
+        }
 
-      router.removeTarget(content)
-      fanout.remove(content)
+        router.removeTarget(content);
+        fanout.remove(content);
 
-      content.destroy()
+        content.destroy();
 
-      content = null
-    }
-
-    function addContent(K) {
-      removeContent()
-
-      content = new K(config, linkScale, sidebar.getWidth, router, buttons)
-      content.render(contentDiv)
-
-      fanout.add(content)
-      router.addTarget(content)
-    }
-
-    function mkView(K) {
-      return function () {
-        addContent(K)
+        content = null;
       }
-    }
 
-    var loader = document.getElementsByClassName("loader")[0]
-    loader.classList.add("hide")
+      function addContent(K) {
+        removeContent();
 
-    contentDiv = document.createElement("div")
-    contentDiv.classList.add("content")
-    document.body.appendChild(contentDiv)
+        content = new K(config, linkScale, sidebar.getWidth, router, buttons);
+        content.render(contentDiv);
 
-    sidebar = new Sidebar(document.body)
+        fanout.add(content);
+        router.addTarget(content);
+      }
 
-    contentDiv.appendChild(buttons)
+      function mkView(K) {
+        return function () {
+          addContent(K);
+        };
+      }
 
-    var buttonToggle = document.createElement("button")
-    buttonToggle.textContent = "\uF133"
-    buttonToggle.onclick = function () {
-      if (content.constructor === Map)
-        router.view("g")
-      else
-        router.view("m")
-    }
+      var loader = document.getElementsByClassName("loader")[0];
+      loader.classList.add("hide");
 
-    buttons.appendChild(buttonToggle)
+      contentDiv = document.createElement("div");
+      contentDiv.classList.add("content");
+      document.body.appendChild(contentDiv);
 
-    var title = new Title(config)
+      sidebar = new Sidebar(document.body);
 
-    var header = new Container("header")
-    var infobox = new Infobox(config, sidebar, router)
-    var tabs = new Tabs()
-    var overview = new Container()
-    var meshstats = new Meshstats(config)
-    var legend = new Legend()
-    var newnodeslist = new SimpleNodelist("new", "firstseen", router, "Neue Knoten")
-    var lostnodeslist = new SimpleNodelist("lost", "lastseen", router, "Verschwundene Knoten")
-    var nodelist = new Nodelist(router)
-    var linklist = new Linklist(linkScale, router)
-    var statistics = new Proportions(config, fanout)
-    var about = new About()
+      contentDiv.appendChild(buttons);
 
-    fanoutUnfiltered.add(meshstats)
-    fanoutUnfiltered.add(newnodeslist)
-    fanoutUnfiltered.add(lostnodeslist)
-    fanout.add(nodelist)
-    fanout.add(linklist)
-    fanout.add(statistics)
+      var buttonToggle = document.createElement("button");
+      buttonToggle.textContent = "\uF133";
+      buttonToggle.onclick = function () {
+        if (content.constructor === Map) {
+          router.view("g");
+        } else {
+          router.view("m");
+        }
+      };
 
-    sidebar.add(header)
-    header.add(meshstats)
-    header.add(legend)
+      buttons.appendChild(buttonToggle);
 
-    overview.add(newnodeslist)
-    overview.add(lostnodeslist)
+      var title = new Title(config);
 
-    var filterGUI = new FilterGUI(fanout)
-    fanout.watchFilters(filterGUI)
-    header.add(filterGUI)
+      var header = new Container("header");
+      var infobox = new Infobox(config, sidebar, router);
+      var tabs = new Tabs();
+      var overview = new Container();
+      var meshstats = new Meshstats(config);
+      var legend = new Legend();
+      var newnodeslist = new SimpleNodelist("new", "firstseen", router, "Neue Knoten");
+      var lostnodeslist = new SimpleNodelist("lost", "lastseen", router, "Verschwundene Knoten");
+      var nodelist = new Nodelist(router);
+      var linklist = new Linklist(linkScale, router);
+      var statistics = new Proportions(config, fanout);
+      var about = new About();
 
-    sidebar.add(tabs)
-    tabs.add("Aktuelles", overview)
-    tabs.add("Knoten", nodelist)
-    tabs.add("Verbindungen", linklist)
-    tabs.add("Statistiken", statistics)
-    tabs.add("Über", about)
+      fanoutUnfiltered.add(meshstats);
+      fanoutUnfiltered.add(newnodeslist);
+      fanoutUnfiltered.add(lostnodeslist);
+      fanout.add(nodelist);
+      fanout.add(linklist);
+      fanout.add(statistics);
 
-    router.addTarget(title)
-    router.addTarget(infobox)
+      sidebar.add(header);
+      header.add(meshstats);
+      header.add(legend);
 
-    router.addView("m", mkView(Map))
-    router.addView("g", mkView(ForceGraph))
+      overview.add(newnodeslist);
+      overview.add(lostnodeslist);
 
-    router.view("m")
+      var filterGUI = new FilterGUI(fanout);
+      fanout.watchFilters(filterGUI);
+      header.add(filterGUI);
 
-    self.setData = fanoutUnfiltered.setData
+      sidebar.add(tabs);
+      tabs.add("Aktuelles", overview);
+      tabs.add("Knoten", nodelist);
+      tabs.add("Verbindungen", linklist);
+      tabs.add("Statistiken", statistics);
+      tabs.add("Über", about);
 
-    return self
-  }
-})
+      router.addTarget(title);
+      router.addTarget(infobox);
+
+      router.addView("m", mkView(Map));
+      router.addView("g", mkView(ForceGraph));
+
+      router.view("m");
+
+      self.setData = fanoutUnfiltered.setData;
+
+      return self;
+    };
+  });
diff --git a/lib/infobox/link.js b/lib/infobox/link.js
index 82b2d9f..2d5e8c0 100644
--- a/lib/infobox/link.js
+++ b/lib/infobox/link.js
@@ -1,48 +1,49 @@
 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)
+    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) {
-    var unknown = !(d.source.node)
-    var h2 = document.createElement("h2")
-    var a1 = document.createElement("a")
+    var unknown = !(d.source.node);
+    var h2 = document.createElement("h2");
+    var a1 = document.createElement("a");
     if (!unknown) {
-      a1.href = "#"
-      a1.onclick = router.node(d.source.node)
+      a1.href = "#";
+      a1.onclick = router.node(d.source.node);
     }
-    a1.textContent = unknown ? d.source.id : d.source.node.nodeinfo.hostname
-    h2.appendChild(a1)
-    h2.appendChild(document.createTextNode(" → "))
-    var a2 = document.createElement("a")
-    a2.href = "#"
-    a2.onclick = router.node(d.target.node)
-    a2.textContent = d.target.node.nodeinfo.hostname
-    h2.appendChild(a2)
-    el.appendChild(h2)
+    a1.textContent = unknown ? d.source.id : d.source.node.nodeinfo.hostname;
+    h2.appendChild(a1);
+    h2.appendChild(document.createTextNode(" → "));
+    var a2 = document.createElement("a");
+    a2.href = "#";
+    a2.onclick = router.node(d.target.node);
+    a2.textContent = d.target.node.nodeinfo.hostname;
+    h2.appendChild(a2);
+    el.appendChild(h2);
 
-    var attributes = document.createElement("table")
-    attributes.classList.add("attributes")
+    var attributes = document.createElement("table");
+    attributes.classList.add("attributes");
 
-    attributeEntry(attributes, "TQ", showTq(d))
-    attributeEntry(attributes, "Entfernung", showDistance(d))
-    attributeEntry(attributes, "Typ", d.type)
-    var hw1 = unknown ? null : dictGet(d.source.node.nodeinfo, ["hardware", "model"])
-    var hw2 = dictGet(d.target.node.nodeinfo, ["hardware", "model"])
-    attributeEntry(attributes, "Hardware", (hw1 != null ? hw1 : "unbekannt") + " – " + (hw2 != null ? hw2 : "unbekannt"))
-    el.appendChild(attributes)
+    attributeEntry(attributes, "TQ", showTq(d));
+    attributeEntry(attributes, "Entfernung", showDistance(d));
+    attributeEntry(attributes, "Typ", d.type);
+    var hw1 = unknown ? null : dictGet(d.source.node.nodeinfo, ["hardware", "model"]);
+    var hw2 = dictGet(d.target.node.nodeinfo, ["hardware", "model"]);
+    attributeEntry(attributes, "Hardware", (hw1 != null ? hw1 : "unbekannt") + " – " + (hw2 != null ? hw2 : "unbekannt"));
+    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))
-      })
-  }
-})
+    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));
+      });
+    }
+  };
+});
diff --git a/lib/infobox/location.js b/lib/infobox/location.js
index 08805b0..9910d33 100644
--- a/lib/infobox/location.js
+++ b/lib/infobox/location.js
@@ -1,100 +1,103 @@
 define(function () {
   return function (config, el, router, d) {
-    var sidebarTitle = document.createElement("h2")
-    sidebarTitle.textContent = "Location: " + d.toString()
-    el.appendChild(sidebarTitle)
+    var sidebarTitle = document.createElement("h2");
+    sidebarTitle.textContent = "Location: " + d.toString();
+    el.appendChild(sidebarTitle);
 
     getJSON("https://nominatim.openstreetmap.org/reverse?format=json&lat=" + d.lat + "&lon=" + d.lng + "&zoom=18&addressdetails=0")
-    .then(function(result) {
-      if(result.display_name)
-        sidebarTitle.textContent = result.display_name
-    })
+      .then(function (result) {
+        if (result.display_name) {
+          sidebarTitle.textContent = result.display_name;
+        }
+      });
 
-    var editLat = document.createElement("input")
-    editLat.type = "text"
-    editLat.value = d.lat.toFixed(9)
-    el.appendChild(createBox("lat", "Breitengrad", editLat))
+    var editLat = document.createElement("input");
+    editLat.type = "text";
+    editLat.value = d.lat.toFixed(9);
+    el.appendChild(createBox("lat", "Breitengrad", editLat));
 
-    var editLng = document.createElement("input")
-    editLng.type = "text"
-    editLng.value = d.lng.toFixed(9)
-    el.appendChild(createBox("lng", "Längengrad", editLng))
+    var editLng = document.createElement("input");
+    editLng.type = "text";
+    editLng.value = d.lng.toFixed(9);
+    el.appendChild(createBox("lng", "Längengrad", editLng));
 
-    var editUci = document.createElement("textarea")
+    var editUci = document.createElement("textarea");
     editUci.value =
       "uci set gluon-node-info.@location[0]='location'; " +
       "uci set gluon-node-info.@location[0].share_location='1';" +
       "uci set gluon-node-info.@location[0].latitude='" + d.lat.toFixed(9) + "';" +
       "uci set gluon-node-info.@location[0].longitude='" + d.lng.toFixed(9) + "';" +
-      "uci commit gluon-node-info"
+      "uci commit gluon-node-info";
 
-    el.appendChild(createBox("uci", "Befehl", editUci, false))
+    el.appendChild(createBox("uci", "Befehl", editUci, false));
 
-    var linkPlain = document.createElement("a")
-    linkPlain.textContent = "plain"
-    linkPlain.onclick = function() {
-      switch2plain()
-      return false
-    }
-    linkPlain.href = "#"
+    var linkPlain = document.createElement("a");
+    linkPlain.textContent = "plain";
+    linkPlain.onclick = function () {
+      switch2plain();
+      return false;
+    };
+    linkPlain.href = "#";
 
-    var linkUci = document.createElement("a")
-    linkUci.textContent = "uci"
-    linkUci.onclick = function() {
-      switch2uci()
-      return false
-    }
-    linkUci.href = "#"
+    var linkUci = document.createElement("a");
+    linkUci.textContent = "uci";
+    linkUci.onclick = function () {
+      switch2uci();
+      return false;
+    };
+    linkUci.href = "#";
 
-    var hintText = document.createElement("p")
-    hintText.appendChild(document.createTextNode("Du kannst zwischen "))
-    hintText.appendChild(linkPlain)
-    hintText.appendChild(document.createTextNode(" und "))
-    hintText.appendChild(linkUci)
-    hintText.appendChild(document.createTextNode(" wechseln."))
-    el.appendChild(hintText)
+    var hintText = document.createElement("p");
+    hintText.appendChild(document.createTextNode("Du kannst zwischen "));
+    hintText.appendChild(linkPlain);
+    hintText.appendChild(document.createTextNode(" und "));
+    hintText.appendChild(linkUci);
+    hintText.appendChild(document.createTextNode(" wechseln."));
+    el.appendChild(hintText);
 
     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")
-      btn.className = "ion-ios-copy"
-      btn.title = "Kopieren"
-      btn.onclick = function() { copy2clip(inputElem.id) }
-      inputElem.id = "location-" + name
-      inputElem.readOnly = true
-      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
+      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");
+      btn.className = "ion-ios-copy";
+      btn.title = "Kopieren";
+      btn.onclick = function () {
+        copy2clip(inputElem.id);
+      };
+      inputElem.id = "location-" + name;
+      inputElem.readOnly = true;
+      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) {
-      var copyField = document.querySelector("#" + id)
-      copyField.select()
+      var copyField = document.querySelector("#" + id);
+      copyField.select();
       try {
-        document.execCommand("copy")
+        document.execCommand("copy");
       } catch (err) {
-        console.log(err)
+        console.log(err);
       }
     }
 
     function switch2plain() {
-      document.getElementById("box-uci").style.display = "none"
-      document.getElementById("box-lat").style.display = "block"
-      document.getElementById("box-lng").style.display = "block"
+      document.getElementById("box-uci").style.display = "none";
+      document.getElementById("box-lat").style.display = "block";
+      document.getElementById("box-lng").style.display = "block";
     }
 
     function switch2uci() {
-      document.getElementById("box-uci").style.display = "block"
-      document.getElementById("box-lat").style.display = "none"
-      document.getElementById("box-lng").style.display = "none"
+      document.getElementById("box-uci").style.display = "block";
+      document.getElementById("box-lat").style.display = "none";
+      document.getElementById("box-lng").style.display = "none";
     }
-  }
-})
+  };
+});
diff --git a/lib/infobox/main.js b/lib/infobox/main.js
index deebc03..dc900e4 100644
--- a/lib/infobox/main.js
+++ b/lib/infobox/main.js
@@ -1,51 +1,51 @@
 define(["infobox/link", "infobox/node", "infobox/location"], function (Link, Node, Location) {
   return function (config, sidebar, router) {
-    var self = this
-    var el
+    var self = this;
+    var el;
 
     function destroy() {
       if (el && el.parentNode) {
-        el.parentNode.removeChild(el)
-        el = undefined
-        sidebar.reveal()
+        el.parentNode.removeChild(el);
+        el = undefined;
+        sidebar.reveal();
       }
     }
 
     function create() {
-      destroy()
-      sidebar.ensureVisible()
-      sidebar.hide()
+      destroy();
+      sidebar.ensureVisible();
+      sidebar.hide();
 
-      el = document.createElement("div")
-      sidebar.container.insertBefore(el, sidebar.container.firstChild)
+      el = document.createElement("div");
+      sidebar.container.insertBefore(el, sidebar.container.firstChild);
 
-      el.scrollIntoView(false)
-      el.classList.add("infobox")
-      el.destroy = destroy
+      el.scrollIntoView(false);
+      el.classList.add("infobox");
+      el.destroy = destroy;
 
-      var closeButton = document.createElement("button")
-      closeButton.classList.add("close")
-      closeButton.onclick = router.reset
-      el.appendChild(closeButton)
+      var closeButton = document.createElement("button");
+      closeButton.classList.add("close");
+      closeButton.onclick = router.reset;
+      el.appendChild(closeButton);
     }
 
-    self.resetView = destroy
+    self.resetView = destroy;
 
     self.gotoNode = function (d) {
-      create()
-      new Node(config, el, router, d)
-    }
+      create();
+      new Node(config, el, router, d);
+    };
 
     self.gotoLink = function (d) {
-      create()
-      new Link(config, el, router, d)
-    }
+      create();
+      new Link(config, el, router, d);
+    };
 
     self.gotoLocation = function (d) {
-      create()
-      new Location(config, el, router, d)
-    }
+      create();
+      new Location(config, el, router, d);
+    };
 
-    return self
-  }
-})
+    return self;
+  };
+});
diff --git a/lib/infobox/node.js b/lib/infobox/node.js
index 34e1ee2..3654c3d 100644
--- a/lib/infobox/node.js
+++ b/lib/infobox/node.js
@@ -1,581 +1,631 @@
 define(["moment", "numeral", "tablesort", "tablesort.numeric"],
   function (moment, numeral, Tablesort) {
-  function showGeoURI(d) {
-    function showLatitude(d) {
-      var suffix = Math.sign(d) > -1 ? "' N" : "' S"
-      d = Math.abs(d)
-      var a = Math.floor(d)
-      var min = (d * 60) % 60
-      a = (a < 10 ? "0" : "") + a
+    function showGeoURI(d) {
+      function showLatitude(d) {
+        var suffix = Math.sign(d) > -1 ? "' N" : "' S";
+        d = Math.abs(d);
+        var a = Math.floor(d);
+        var min = (d * 60) % 60;
+        a = (a < 10 ? "0" : "") + a;
 
-      return a + "° " + numeral(min).format("0.000") + suffix
-    }
-
-    function showLongitude(d) {
-      var suffix = Math.sign(d) > -1 ? "' E" : "' W"
-      d = Math.abs(d)
-      var a = Math.floor(d)
-      var min = (d * 60) % 60
-      a = (a < 100 ? "0" + (a < 10 ? "0" : "") : "") + a
-
-      return a + "° " + numeral(min).format("0.000") + suffix
-    }
-
-    if (!has_location(d))
-      return undefined
-
-    return function (el) {
-      var latitude = d.nodeinfo.location.latitude
-      var longitude = d.nodeinfo.location.longitude
-      var a = document.createElement("a")
-      a.textContent = showLatitude(latitude) + " " +
-                      showLongitude(longitude)
-
-      a.href = "geo:" + latitude + "," + longitude
-      el.appendChild(a)
-    }
-  }
-
-  function showStatus(d) {
-    return function (el) {
-      el.classList.add(d.flags.unseen ? "unseen" : (d.flags.online ? "online" : "offline"))
-      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") + ")"
-    }
-  }
-
-  function showFirmware(d) {
-    var release = dictGet(d.nodeinfo, ["software", "firmware", "release"])
-    var base = dictGet(d.nodeinfo, ["software", "firmware", "base"])
-
-    if (release === null || base === null)
-      return undefined
-
-    return release + " / " + base
-  }
-
-  function showSite(d, config) {
-    var site = dictGet(d.nodeinfo, ["system", "site_code"])
-    var rt = site
-    if (config.siteNames)
-      config.siteNames.forEach( function (t) {
-        if(site === t.site)
-          rt = t.name
-      })
-    return rt
-  }
-
-  function showUptime(d) {
-    if (!("uptime" in d.statistics))
-      return undefined
-
-    return moment.duration(d.statistics.uptime, "seconds").humanize()
-  }
-
-  function showFirstseen(d) {
-    if (!("firstseen" in d))
-      return undefined
-
-    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) {
-    if (!d.flags.online)
-      return undefined
-
-    var meshclients = getMeshClients(d)
-    resetMeshClients(d)
-    var before = "     ("
-    var after = " in der lokalen Wolke)"
-    return function (el) {
-      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"))
-
-      var span = document.createElement("span")
-      span.classList.add("clients")
-      span.textContent = " ".repeat(d.statistics.clients)
-      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"))
-    }
-  }
-
-  function showIPs(d) {
-    var ips = dictGet(d.nodeinfo, ["network", "addresses"])
-    if (ips === null)
-      return undefined
-
-    ips.sort()
-
-    return function (el) {
-      ips.forEach( function (ip, i) {
-        var link = !ip.startsWith("fe80:")
-
-        if (i > 0)
-          el.appendChild(document.createElement("br"))
-
-        if (link) {
-          var a = document.createElement("a")
-          if (ip.includes("."))
-            a.href = "http://" + ip + "/"
-          else
-            a.href = "http://[" + ip + "]/"
-          a.textContent = ip
-          el.appendChild(a)
-        } else
-          el.appendChild(document.createTextNode(ip))
-      })
-    }
-  }
-
-  function showBar(className, v) {
-    var span = document.createElement("span")
-    span.classList.add("bar")
-    span.classList.add(className)
-
-    var bar = document.createElement("span")
-    bar.style.width = (v * 100) + "%"
-    span.appendChild(bar)
-
-    var label = document.createElement("label")
-    label.textContent = (Math.round(v * 100)) + " %"
-    span.appendChild(label)
-
-    return span
-  }
-
-  function showLoadBar(className, v) {
-    var span = document.createElement("span")
-    span.classList.add("bar")
-    span.classList.add(className)
-
-    var bar = document.createElement("span")
-    if (v  >= 1) {
-    bar.style.width = ((v * 100) % 100) + "%"
-    bar.style.background = "rgba(255, 50, 50, 0.9)"
-    span.style.background = "rgba(255, 50, 50, 0.6)"
-    span.appendChild(bar)
-    }
-    else
-    {
-      bar.style.width = (v * 100) + "%"
-      span.appendChild(bar)
-    }
-
-    var label = document.createElement("label")
-    label.textContent = +(Math.round(v + "e+2")  + "e-2")
-    span.appendChild(label)
-
-    return span
-  }
-
-  function showLoad(d) {
-    if (!("loadavg" in d.statistics))
-      return undefined
-
-    return function (el) {
-      el.appendChild(showLoadBar("load-avg", d.statistics.loadavg))
-    }
-  }
-
-  function showRAM(d) {
-    if (!("memory_usage" in d.statistics))
-      return undefined
-
-    return function (el) {
-      el.appendChild(showBar("memory-usage", d.statistics.memory_usage))
-    }
-  }
-
-  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
+        return a + "° " + numeral(min).format("0.000") + suffix;
       }
-      if (gw && nh && gw.id !== nh.id) {
-        if (num !== 0) el.appendChild(document.createTextNode(" -> "))
-        num++
-        el.appendChild(document.createTextNode("..."))
+
+      function showLongitude(d) {
+        var suffix = Math.sign(d) > -1 ? "' E" : "' W";
+        d = Math.abs(d);
+        var a = Math.floor(d);
+        var min = (d * 60) % 60;
+        a = (a < 100 ? "0" + (a < 10 ? "0" : "") : "") + a;
+
+        return a + "° " + numeral(min).format("0.000") + suffix;
       }
-      if (num !== 0) el.appendChild(document.createTextNode(" -> "))
-      el.appendChild(createLink(gw, router))
-    }
-  }
 
-  function showPages(d) {
-    var webpages = dictGet(d.nodeinfo, ["pages"])
-    if (webpages === null)
-      return undefined
-
-    webpages.sort()
-
-    return function (el) {
-      webpages.forEach( function (webpage, i) {
-        if (i > 0)
-          el.appendChild(document.createElement("br"))
-
-        var a = document.createElement("span")
-        var link = document.createElement("a")
-        link.href = webpage
-        if (webpage.search(/^https:\/\//i) !== -1) {
-          var lock = document.createElement("span")
-          lock.className = "ion-android-lock"
-          a.appendChild(lock)
-          var t1 = document.createTextNode(" ")
-          a.appendChild(t1)
-          link.textContent = webpage.replace(/^https:\/\//i, "")
-          }
-        else
-          link.textContent = webpage.replace(/^http:\/\//i, "")
-        a.appendChild(link)
-        el.appendChild(a)
-      })
-    }
-  }
-
-  function showAutoupdate(d) {
-    var au = dictGet(d.nodeinfo, ["software", "autoupdater"])
-    if (!au)
-      return undefined
-
-    return au.enabled ? "aktiviert (" + au.branch + ")" : "deaktiviert"
-  }
-
-  function showNodeImg(o, model) {
-    if (!model)
-      return document.createTextNode("Knotenname")
-
-    var content, caption
-    var modelhash = model.split("").reduce(function(a, b) {
-      a = ((a << 5) - a) + b.charCodeAt(0)
-      return a & a
-    }, 0)
-
-    content = document.createElement("img")
-    content.id = "routerpicture"
-    content.classList.add("nodeImg")
-    content.src = o.thumbnail.replace("{MODELHASH}", modelhash)
-    content.onerror = function() {
-      document.getElementById("routerpicdiv").outerHTML = "Knotenname"
-    }
-
-    if (o.caption) {
-      caption = o.caption.replace("{MODELHASH}", modelhash)
-
-      if (!content)
-        content = document.createTextNode(caption)
-    }
-
-    var p = document.createElement("p")
-    p.appendChild(content)
-
-    return content
-  }
-
-  function showStatImg(o, d) {
-    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) {
-    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)
+      if (!has_location(d)) {
+        return undefined;
       }
-      attributeEntry(attributes, top, d.nodeinfo.hostname)
-    } else {
-      var h2 = document.createElement("h2")
-      h2.textContent = d.nodeinfo.hostname
-      el.appendChild(h2)
+
+      return function (el) {
+        var latitude = d.nodeinfo.location.latitude;
+        var longitude = d.nodeinfo.location.longitude;
+        var a = document.createElement("a");
+        a.textContent = showLatitude(latitude) + " " +
+          showLongitude(longitude);
+
+        a.href = "geo:" + latitude + "," + longitude;
+        el.appendChild(a);
+      };
     }
 
-    attributeEntry(attributes, "Status", showStatus(d))
-    attributeEntry(attributes, "Gateway", d.flags.gateway ? "ja" : null)
-    attributeEntry(attributes, "Koordinaten", showGeoURI(d))
-
-    if (config.showContact)
-      attributeEntry(attributes, "Kontakt", dictGet(d.nodeinfo, ["owner", "contact"]))
-
-    attributeEntry(attributes, "Hardware",  dictGet(d.nodeinfo, ["hardware", "model"]))
-    attributeEntry(attributes, "Primäre MAC", dictGet(d.nodeinfo, ["network", "mac"]))
-    attributeEntry(attributes, "Node ID", dictGet(d.nodeinfo, ["node_id"]))
-    attributeEntry(attributes, "Firmware", showFirmware(d))
-    attributeEntry(attributes, "Site", showSite(d, config))
-    attributeEntry(attributes, "Uptime", showUptime(d))
-    attributeEntry(attributes, "Teil des Netzes", showFirstseen(d))
-    attributeEntry(attributes, "Kanal 2.4 GHz", showWifiChannel(dictGet(d.nodeinfo, ["wireless", "chan2"])))
-    attributeEntry(attributes, "Kanal 5 GHz", showWifiChannel(dictGet(d.nodeinfo, ["wireless", "chan5"])))
-    attributeEntry(attributes, "Airtime 2.4 GHz", showAirtime(2, dictGet(d.statistics, ["wireless", "airtime2"])))
-    attributeEntry(attributes, "Airtime 5 GHz", showAirtime(5, dictGet(d.statistics, ["wireless", "airtime5"])))
-    attributeEntry(attributes, "Systemlast", showLoad(d))
-    attributeEntry(attributes, "Arbeitsspeicher", showRAM(d))
-    attributeEntry(attributes, "IP Adressen", showIPs(d))
-    attributeEntry(attributes, "Webseite", showPages(d))
-    attributeEntry(attributes, "Gewähltes Gateway", showGateway(d, router))
-    attributeEntry(attributes, "Autom. Updates", showAutoupdate(d))
-    attributeEntry(attributes, "Clients", showClients(d), showMeshClients(d))
-
-    el.appendChild(attributes)
-
-
-    if (config.nodeInfos)
-      config.nodeInfos.forEach( function (nodeInfo) {
-        var h4 = document.createElement("h4")
-        h4.textContent = nodeInfo.name
-        el.appendChild(h4)
-        el.appendChild(showStatImg(nodeInfo, d))
-      })
-
-    if (d.neighbours.length > 0) {
-      var h3 = document.createElement("h3")
-      h3.textContent = "Links (" + d.neighbours.length + ")"
-      el.appendChild(h3)
-
-      var table = document.createElement("table")
-      var thead = document.createElement("thead")
-
-      var tr = document.createElement("tr")
-      var th1 = document.createElement("th")
-      th1.textContent = " "
-      tr.appendChild(th1)
-
-      var th2 = document.createElement("th")
-      th2.textContent = "Knoten"
-      th2.classList.add("sort-default")
-      tr.appendChild(th2)
-
-      var th3 = document.createElement("th")
-      th3.textContent = "TQ"
-      tr.appendChild(th3)
-
-      var th4 = document.createElement("th")
-      th4.textContent = "Typ"
-      tr.appendChild(th4)
-
-      var th5 = document.createElement("th")
-      th5.textContent = "Entfernung"
-      tr.appendChild(th5)
-
-      thead.appendChild(tr)
-      table.appendChild(thead)
-
-      var tbody = document.createElement("tbody")
-
-      d.neighbours.forEach( function (d) {
-        var unknown = !(d.node)
-        var tr = document.createElement("tr")
-
-        var td1 = document.createElement("td")
-        td1.appendChild(document.createTextNode(d.incoming ? " ← " : " → "))
-        tr.appendChild(td1)
-
-        var td2 = document.createElement("td")
-        td2.appendChild(createLink(d, router))
-
-        if (!unknown && has_location(d.node)) {
-          var span = document.createElement("span")
-          span.classList.add("icon")
-          span.classList.add("ion-location")
-          td2.appendChild(span)
+    function showStatus(d) {
+      return function (el) {
+        el.classList.add(d.flags.unseen ? "unseen" : (d.flags.online ? "online" : "offline"));
+        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") + ")";
         }
-
-        tr.appendChild(td2)
-
-        var td3 = document.createElement("td")
-        var a2 = document.createElement("a")
-        a2.href = "#"
-        a2.textContent = showTq(d.link)
-        a2.onclick = router.link(d.link)
-        td3.appendChild(a2)
-        tr.appendChild(td3)
-
-        var td4 = document.createElement("td")
-        var a3 = document.createElement("a")
-        a3.href = "#"
-        a3.textContent = d.link.type
-        a3.onclick = router.link(d.link)
-        td4.appendChild(a3)
-        tr.appendChild(td4)
-
-        var td5 = document.createElement("td")
-        var a4 = document.createElement("a")
-        a4.href = "#"
-        a4.textContent = showDistance(d.link)
-        a4.onclick = router.link(d.link)
-        td5.appendChild(a4)
-        td5.setAttribute("data-sort", d.link.distance !== undefined ? -d.link.distance : 1)
-        tr.appendChild(td5)
-
-        tbody.appendChild(tr)
-      })
-
-      table.appendChild(tbody)
-      table.className = "node-links"
-
-      new Tablesort(table)
-
-      el.appendChild(table)
+      };
     }
-  }
-})
+
+    function showFirmware(d) {
+      var release = dictGet(d.nodeinfo, ["software", "firmware", "release"]);
+      var base = dictGet(d.nodeinfo, ["software", "firmware", "base"]);
+
+      if (release === null || base === null) {
+        return undefined;
+      }
+
+      return release + " / " + base;
+    }
+
+    function showSite(d, config) {
+      var site = dictGet(d.nodeinfo, ["system", "site_code"]);
+      var rt = site;
+      if (config.siteNames) {
+        config.siteNames.forEach(function (t) {
+          if (site === t.site) {
+            rt = t.name;
+          }
+        });
+      }
+      return rt;
+    }
+
+    function showUptime(d) {
+      if (!("uptime" in d.statistics)) {
+        return undefined;
+      }
+
+      return moment.duration(d.statistics.uptime, "seconds").humanize();
+    }
+
+    function showFirstseen(d) {
+      if (!("firstseen" in d)) {
+        return undefined;
+      }
+
+      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) {
+      if (!d.flags.online) {
+        return undefined;
+      }
+
+      var meshclients = getMeshClients(d);
+      resetMeshClients(d);
+      var before = "     (";
+      var after = " in der lokalen Wolke)";
+      return function (el) {
+        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"));
+
+        var span = document.createElement("span");
+        span.classList.add("clients");
+        span.textContent = " ".repeat(d.statistics.clients);
+        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);
+        }
+      });
+
+
+    }
+
+    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"));
+      };
+    }
+
+    function showIPs(d) {
+      var ips = dictGet(d.nodeinfo, ["network", "addresses"]);
+      if (ips === null) {
+        return undefined;
+      }
+
+      ips.sort();
+
+      return function (el) {
+        ips.forEach(function (ip, i) {
+          var link = !ip.startsWith("fe80:");
+
+          if (i > 0) {
+            el.appendChild(document.createElement("br"));
+          }
+
+          if (link) {
+            var a = document.createElement("a");
+            if (ip.includes(".")) {
+              a.href = "http://" + ip + "/";
+            } else {
+              a.href = "http://[" + ip + "]/";
+            }
+            a.textContent = ip;
+            el.appendChild(a);
+          } else {
+            el.appendChild(document.createTextNode(ip));
+          }
+        });
+      };
+    }
+
+    function showBar(className, v) {
+      var span = document.createElement("span");
+      span.classList.add("bar");
+      span.classList.add(className);
+
+      var bar = document.createElement("span");
+      bar.style.width = (v * 100) + "%";
+      span.appendChild(bar);
+
+      var label = document.createElement("label");
+      label.textContent = (Math.round(v * 100)) + " %";
+      span.appendChild(label);
+
+      return span;
+    }
+
+    function showLoadBar(className, v) {
+      var span = document.createElement("span");
+      span.classList.add("bar");
+      span.classList.add(className);
+
+      var bar = document.createElement("span");
+      if (v >= 1) {
+        bar.style.width = ((v * 100) % 100) + "%";
+        bar.style.background = "rgba(255, 50, 50, 0.9)";
+        span.style.background = "rgba(255, 50, 50, 0.6)";
+        span.appendChild(bar);
+      }
+      else {
+        bar.style.width = (v * 100) + "%";
+        span.appendChild(bar);
+      }
+
+      var label = document.createElement("label");
+      label.textContent = +(Math.round(v + "e+2") + "e-2");
+      span.appendChild(label);
+
+      return span;
+    }
+
+    function showLoad(d) {
+      if (!("loadavg" in d.statistics)) {
+        return undefined;
+      }
+
+      return function (el) {
+        el.appendChild(showLoadBar("load-avg", d.statistics.loadavg));
+      };
+    }
+
+    function showRAM(d) {
+      if (!("memory_usage" in d.statistics)) {
+        return undefined;
+      }
+
+      return function (el) {
+        el.appendChild(showBar("memory-usage", d.statistics.memory_usage));
+      };
+    }
+
+    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) {
+      var webpages = dictGet(d.nodeinfo, ["pages"]);
+      if (webpages === null) {
+        return undefined;
+      }
+
+      webpages.sort();
+
+      return function (el) {
+        webpages.forEach(function (webpage, i) {
+          if (i > 0) {
+            el.appendChild(document.createElement("br"));
+          }
+
+          var a = document.createElement("span");
+          var link = document.createElement("a");
+          link.href = webpage;
+          if (webpage.search(/^https:\/\//i) !== -1) {
+            var lock = document.createElement("span");
+            lock.className = "ion-android-lock";
+            a.appendChild(lock);
+            var t1 = document.createTextNode(" ");
+            a.appendChild(t1);
+            link.textContent = webpage.replace(/^https:\/\//i, "");
+          }
+          else {
+            link.textContent = webpage.replace(/^http:\/\//i, "");
+          }
+          a.appendChild(link);
+          el.appendChild(a);
+        });
+      };
+    }
+
+    function showAutoupdate(d) {
+      var au = dictGet(d.nodeinfo, ["software", "autoupdater"]);
+      if (!au) {
+        return undefined;
+      }
+
+      return au.enabled ? "aktiviert (" + au.branch + ")" : "deaktiviert";
+    }
+
+    function showNodeImg(o, model) {
+      if (!model) {
+        return document.createTextNode("Knotenname");
+      }
+
+      var content, caption;
+      var modelhash = model.split("").reduce(function (a, b) {
+        a = ((a << 5) - a) + b.charCodeAt(0);
+        return a & a;
+      }, 0);
+
+      content = document.createElement("img");
+      content.id = "routerpicture";
+      content.classList.add("nodeImg");
+      content.src = o.thumbnail.replace("{MODELHASH}", modelhash);
+      content.onerror = function () {
+        document.getElementById("routerpicdiv").outerHTML = "Knotenname";
+      };
+
+      if (o.caption) {
+        caption = o.caption.replace("{MODELHASH}", modelhash);
+
+        if (!content) {
+          content = document.createTextNode(caption);
+        }
+      }
+
+      var p = document.createElement("p");
+      p.appendChild(content);
+
+      return content;
+    }
+
+    function showStatImg(o, d) {
+      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) {
+      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");
+        h2.textContent = d.nodeinfo.hostname;
+        el.appendChild(h2);
+      }
+
+      attributeEntry(attributes, "Status", showStatus(d));
+      attributeEntry(attributes, "Gateway", d.flags.gateway ? "ja" : null);
+      attributeEntry(attributes, "Koordinaten", showGeoURI(d));
+
+      if (config.showContact) {
+        attributeEntry(attributes, "Kontakt", dictGet(d.nodeinfo, ["owner", "contact"]));
+      }
+
+      attributeEntry(attributes, "Hardware", dictGet(d.nodeinfo, ["hardware", "model"]));
+      attributeEntry(attributes, "Primäre MAC", dictGet(d.nodeinfo, ["network", "mac"]));
+      attributeEntry(attributes, "Node ID", dictGet(d.nodeinfo, ["node_id"]));
+      attributeEntry(attributes, "Firmware", showFirmware(d));
+      attributeEntry(attributes, "Site", showSite(d, config));
+      attributeEntry(attributes, "Uptime", showUptime(d));
+      attributeEntry(attributes, "Teil des Netzes", showFirstseen(d));
+      attributeEntry(attributes, "Kanal 2.4 GHz", showWifiChannel(dictGet(d.nodeinfo, ["wireless", "chan2"])));
+      attributeEntry(attributes, "Kanal 5 GHz", showWifiChannel(dictGet(d.nodeinfo, ["wireless", "chan5"])));
+      attributeEntry(attributes, "Airtime 2.4 GHz", showAirtime(2, dictGet(d.statistics, ["wireless", "airtime2"])));
+      attributeEntry(attributes, "Airtime 5 GHz", showAirtime(5, dictGet(d.statistics, ["wireless", "airtime5"])));
+      attributeEntry(attributes, "Systemlast", showLoad(d));
+      attributeEntry(attributes, "Arbeitsspeicher", showRAM(d));
+      attributeEntry(attributes, "IP Adressen", showIPs(d));
+      attributeEntry(attributes, "Webseite", showPages(d));
+      attributeEntry(attributes, "Gewähltes Gateway", showGateway(d, router));
+      attributeEntry(attributes, "Autom. Updates", showAutoupdate(d));
+      attributeEntry(attributes, "Clients", showClients(d), showMeshClients(d));
+
+      el.appendChild(attributes);
+
+
+      if (config.nodeInfos) {
+        config.nodeInfos.forEach(function (nodeInfo) {
+          var h4 = document.createElement("h4");
+          h4.textContent = nodeInfo.name;
+          el.appendChild(h4);
+          el.appendChild(showStatImg(nodeInfo, d));
+        });
+      }
+
+      if (d.neighbours.length > 0) {
+        var h3 = document.createElement("h3");
+        h3.textContent = "Links (" + d.neighbours.length + ")";
+        el.appendChild(h3);
+
+        var table = document.createElement("table");
+        var thead = document.createElement("thead");
+
+        var tr = document.createElement("tr");
+        var th1 = document.createElement("th");
+        th1.textContent = " ";
+        tr.appendChild(th1);
+
+        var th2 = document.createElement("th");
+        th2.textContent = "Knoten";
+        th2.classList.add("sort-default");
+        tr.appendChild(th2);
+
+        var th3 = document.createElement("th");
+        th3.textContent = "TQ";
+        tr.appendChild(th3);
+
+        var th4 = document.createElement("th");
+        th4.textContent = "Typ";
+        tr.appendChild(th4);
+
+        var th5 = document.createElement("th");
+        th5.textContent = "Entfernung";
+        tr.appendChild(th5);
+
+        thead.appendChild(tr);
+        table.appendChild(thead);
+
+        var tbody = document.createElement("tbody");
+
+        d.neighbours.forEach(function (d) {
+          var unknown = !(d.node);
+          var tr = document.createElement("tr");
+
+          var td1 = document.createElement("td");
+          td1.appendChild(document.createTextNode(d.incoming ? " ← " : " → "));
+          tr.appendChild(td1);
+
+          var td2 = document.createElement("td");
+          td2.appendChild(createLink(d, router));
+
+          if (!unknown && has_location(d.node)) {
+            var span = document.createElement("span");
+            span.classList.add("icon");
+            span.classList.add("ion-location");
+            td2.appendChild(span);
+          }
+
+          tr.appendChild(td2);
+
+          var td3 = document.createElement("td");
+          var a2 = document.createElement("a");
+          a2.href = "#";
+          a2.textContent = showTq(d.link);
+          a2.onclick = router.link(d.link);
+          td3.appendChild(a2);
+          tr.appendChild(td3);
+
+          var td4 = document.createElement("td");
+          var a3 = document.createElement("a");
+          a3.href = "#";
+          a3.textContent = d.link.type;
+          a3.onclick = router.link(d.link);
+          td4.appendChild(a3);
+          tr.appendChild(td4);
+
+          var td5 = document.createElement("td");
+          var a4 = document.createElement("a");
+          a4.href = "#";
+          a4.textContent = showDistance(d.link);
+          a4.onclick = router.link(d.link);
+          td5.appendChild(a4);
+          td5.setAttribute("data-sort", d.link.distance !== undefined ? -d.link.distance : 1);
+          tr.appendChild(td5);
+
+          tbody.appendChild(tr);
+        });
+
+        table.appendChild(tbody);
+        table.className = "node-links";
+
+        new Tablesort(table);
+
+        el.appendChild(table);
+      }
+    };
+  });
diff --git a/lib/legend.js b/lib/legend.js
index bee87c3..b01782f 100644
--- a/lib/legend.js
+++ b/lib/legend.js
@@ -1,41 +1,41 @@
 define(function () {
   return function () {
-    var self = this
+    var self = this;
 
     self.render = function (el) {
-      var p = document.createElement("p")
-      p.setAttribute("class", "legend")
-      el.appendChild(p)
+      var p = document.createElement("p");
+      p.setAttribute("class", "legend");
+      el.appendChild(p);
 
-      var spanNew = document.createElement("span")
-      spanNew.setAttribute("class", "legend-new")
-      var symbolNew = document.createElement("span")
-      symbolNew.setAttribute("class", "symbol")
-      var textNew = document.createTextNode(" Neuer Knoten")
-      spanNew.appendChild(symbolNew)
-      spanNew.appendChild(textNew)
-      p.appendChild(spanNew)
+      var spanNew = document.createElement("span");
+      spanNew.setAttribute("class", "legend-new");
+      var symbolNew = document.createElement("span");
+      symbolNew.setAttribute("class", "symbol");
+      var textNew = document.createTextNode(" Neuer Knoten");
+      spanNew.appendChild(symbolNew);
+      spanNew.appendChild(textNew);
+      p.appendChild(spanNew);
 
-      var spanOnline = document.createElement("span")
-      spanOnline.setAttribute("class", "legend-online")
-      var symbolOnline = document.createElement("span")
-      symbolOnline.setAttribute("class", "symbol")
-      var textOnline = document.createTextNode(" Knoten ist online")
-      spanOnline.appendChild(symbolOnline)
-      spanOnline.appendChild(textOnline)
-      p.appendChild(spanOnline)
+      var spanOnline = document.createElement("span");
+      spanOnline.setAttribute("class", "legend-online");
+      var symbolOnline = document.createElement("span");
+      symbolOnline.setAttribute("class", "symbol");
+      var textOnline = document.createTextNode(" Knoten ist online");
+      spanOnline.appendChild(symbolOnline);
+      spanOnline.appendChild(textOnline);
+      p.appendChild(spanOnline);
 
-      var spanOffline = document.createElement("span")
-      spanOffline.setAttribute("class", "legend-offline")
-      var symbolOffline = document.createElement("span")
-      symbolOffline.setAttribute("class", "symbol")
-      var textOffline = document.createTextNode(" Knoten ist offline")
-      spanOffline.appendChild(symbolOffline)
-      spanOffline.appendChild(textOffline)
-      p.appendChild(spanOffline)
-    }
+      var spanOffline = document.createElement("span");
+      spanOffline.setAttribute("class", "legend-offline");
+      var symbolOffline = document.createElement("span");
+      symbolOffline.setAttribute("class", "symbol");
+      var textOffline = document.createTextNode(" Knoten ist offline");
+      spanOffline.appendChild(symbolOffline);
+      spanOffline.appendChild(textOffline);
+      p.appendChild(spanOffline);
+    };
 
-    return self
-  }
-})
+    return self;
+  };
+});
 
diff --git a/lib/linklist.js b/lib/linklist.js
index 1fc8574..96e7447 100644
--- a/lib/linklist.js
+++ b/lib/linklist.js
@@ -1,60 +1,66 @@
 define(["sorttable", "virtual-dom"], function (SortTable, V) {
   function linkName(d) {
-    return (d.source.node ? d.source.node.nodeinfo.hostname : d.source.id) + " – " + 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",
-                    sort: function (a, b) {
-                      return linkName(a).localeCompare(linkName(b))
-                    },
-                    reverse: false
-                  },
-                  { name: "TQ",
-                    sort: function (a, b) { return a.tq - b.tq},
-                    reverse: true
-                  },
-                  { name: "Typ",
-                    sort: function (a, b) {
-                      return a.type.localeCompare(b.type)
-                    },
-                    reverse: false
-                  },
-                  { name: "Entfernung",
-                    sort: function (a, b) {
-                      return (a.distance === undefined ? -1 : a.distance) -
-                             (b.distance === undefined ? -1 : b.distance)
-                    },
-                    reverse: true
-                  }]
+  var headings = [{
+    name: "Knoten",
+    sort: function (a, b) {
+      return linkName(a).localeCompare(linkName(b));
+    },
+    reverse: false
+  },
+    {
+      name: "TQ",
+      sort: function (a, b) {
+        return a.tq - b.tq;
+      },
+      reverse: true
+    },
+    {
+      name: "Typ",
+      sort: function (a, b) {
+        return a.type.localeCompare(b.type);
+      },
+      reverse: false
+    },
+    {
+      name: "Entfernung",
+      sort: function (a, b) {
+        return (a.distance === undefined ? -1 : a.distance) -
+          (b.distance === undefined ? -1 : b.distance);
+      },
+      reverse: true
+    }];
 
-  return function(linkScale, router) {
-    var table = new SortTable(headings, 2, renderRow)
+  return function (linkScale, router) {
+    var table = new SortTable(headings, 2, renderRow);
 
     function renderRow(d) {
-      var td1Content = [V.h("a", {href: "#", onclick: router.link(d)}, linkName(d))]
+      var td1Content = [V.h("a", {href: "#", onclick: router.link(d)}, linkName(d))];
 
-      var td1 = V.h("td", td1Content)
-      var td2 = V.h("td", {style: {color: linkScale(d.tq).hex()}}, showTq(d))
-      var td3 = V.h("td", d.type)
-      var td4 = V.h("td", showDistance(d))
+      var td1 = V.h("td", td1Content);
+      var td2 = V.h("td", {style: {color: linkScale(d.tq).hex()}}, showTq(d));
+      var td3 = V.h("td", d.type);
+      var td4 = V.h("td", showDistance(d));
 
-      return V.h("tr", [td1, td2, td3, td4])
+      return V.h("tr", [td1, td2, td3, td4]);
     }
 
-    this.render = function (d)  {
-      var el = document.createElement("div")
-      el.last = V.h("div")
-      d.appendChild(el)
+    this.render = function (d) {
+      var el = document.createElement("div");
+      el.last = V.h("div");
+      d.appendChild(el);
 
-      var h2 = document.createElement("h2")
-      h2.textContent = "Verbindungen"
-      el.appendChild(h2)
+      var h2 = document.createElement("h2");
+      h2.textContent = "Verbindungen";
+      el.appendChild(h2);
 
-      el.appendChild(table.el)
-    }
+      el.appendChild(table.el);
+    };
 
     this.setData = function (d) {
-      table.setData(d.graph.links)
-    }
-  }
-})
+      table.setData(d.graph.links);
+    };
+  };
+});
diff --git a/lib/locationmarker.js b/lib/locationmarker.js
index 615eeef..0f8c35a 100644
--- a/lib/locationmarker.js
+++ b/lib/locationmarker.js
@@ -29,31 +29,31 @@ define(["leaflet"], function (L) {
       fillOpacity: 0.2
     },
 
-    initialize: function(latlng) {
-      this.accuracyCircle = L.circle(latlng, 0, this.accuracyCircle)
-      this.outerCircle = L.circleMarker(latlng, this.outerCircle)
-      L.CircleMarker.prototype.initialize.call(this, latlng, this.innerCircle)
+    initialize: function (latlng) {
+      this.accuracyCircle = L.circle(latlng, 0, this.accuracyCircle);
+      this.outerCircle = L.circleMarker(latlng, this.outerCircle);
+      L.CircleMarker.prototype.initialize.call(this, latlng, this.innerCircle);
 
-      this.on("remove", function() {
-        this._map.removeLayer(this.accuracyCircle)
-        this._map.removeLayer(this.outerCircle)
-      })
+      this.on("remove", function () {
+        this._map.removeLayer(this.accuracyCircle);
+        this._map.removeLayer(this.outerCircle);
+      });
     },
 
-    setLatLng: function(latlng) {
-      this.accuracyCircle.setLatLng(latlng)
-      this.outerCircle.setLatLng(latlng)
-      L.CircleMarker.prototype.setLatLng.call(this, latlng)
+    setLatLng: function (latlng) {
+      this.accuracyCircle.setLatLng(latlng);
+      this.outerCircle.setLatLng(latlng);
+      L.CircleMarker.prototype.setLatLng.call(this, latlng);
     },
 
-    setAccuracy: function(accuracy) {
-      this.accuracyCircle.setRadius(accuracy)
+    setAccuracy: function (accuracy) {
+      this.accuracyCircle.setRadius(accuracy);
     },
 
-    onAdd: function(map) {
-      this.accuracyCircle.addTo(map).bringToBack()
-      this.outerCircle.addTo(map)
-      L.CircleMarker.prototype.onAdd.call(this, map)
+    onAdd: function (map) {
+      this.accuracyCircle.addTo(map).bringToBack();
+      this.outerCircle.addTo(map);
+      L.CircleMarker.prototype.onAdd.call(this, map);
     }
-  })
-})
+  });
+});
diff --git a/lib/main.js b/lib/main.js
index eac2a0f..8305891 100644
--- a/lib/main.js
+++ b/lib/main.js
@@ -1,232 +1,241 @@
 define(["moment", "router", "leaflet", "gui", "numeral"],
-function (moment, Router, L, GUI, numeral) {
-  return function (config) {
-    function handleData(data) {
-      var dataNodes = {}
-      dataNodes.nodes = []
-      dataNodes.nodeIds = []
-      var dataGraph = {}
-      dataGraph.batadv = {}
-      dataGraph.batadv.nodes = []
-      dataGraph.batadv.links = []
+  function (moment, Router, L, GUI, numeral) {
+    return function (config) {
+      function handleData(data) {
+        var dataNodes = {};
+        dataNodes.nodes = [];
+        dataNodes.nodeIds = [];
+        var dataGraph = {};
+        dataGraph.batadv = {};
+        dataGraph.batadv.nodes = [];
+        dataGraph.batadv.links = [];
 
 
-
-      function rearrangeLinks(d) {
-        d.source += dataGraph.batadv.nodes.length
-        d.target += dataGraph.batadv.nodes.length
-      }
-
-      for (var i = 0; i < data.length; ++i) {
-        var vererr
-        if(i % 2)
-          if (data[i].version !== 1) {
-            vererr = "Unsupported graph version: " + data[i].version
-            console.log(vererr) //silent fail
-          } else {
-            data[i].batadv.links.forEach(rearrangeLinks)
-            dataGraph.batadv.nodes = dataGraph.batadv.nodes.concat(data[i].batadv.nodes)
-            dataGraph.batadv.links = dataGraph.batadv.links.concat(data[i].batadv.links)
-            dataGraph.timestamp = data[i].timestamp
-          }
-        else
-          if (data[i].version !== 2) {
-            vererr = "Unsupported nodes version: " + data[i].version
-            console.log(vererr) //silent fail
-          } else {
-            data[i].nodes.forEach(fillData)
-            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)
+        function rearrangeLinks(d) {
+          d.source += dataGraph.batadv.nodes.length;
+          d.target += dataGraph.batadv.nodes.length;
         }
-        else
-          if(node.flags.online === true)
-            dataNodes.nodes[position] = node
-      }
 
-      var nodes = dataNodes.nodes.filter( function (d) {
-        return "firstseen" in d && "lastseen" in d
-      })
-
-      nodes.forEach( function(node) {
-        node.firstseen = moment.utc(node.firstseen).local()
-        node.lastseen = moment.utc(node.lastseen).local()
-      })
-
-      var now = moment()
-      var age = moment(now).subtract(config.maxAge, "days")
-
-      var newnodes = limit("firstseen", age, sortByKey("firstseen", nodes).filter(online))
-      var lostnodes = limit("lastseen", age, sortByKey("lastseen", nodes).filter(offline))
-
-      var graphnodes = {}
-
-      dataNodes.nodes.forEach( function (d) {
-        graphnodes[d.nodeinfo.node_id] = d
-      })
-
-      var graph = dataGraph.batadv
-
-      graph.nodes.forEach( function (d) {
-        if (d.node_id in graphnodes) {
-          d.node = graphnodes[d.node_id]
-          if (d.unseen) {
-            d.node.flags.online = true
-            d.node.flags.unseen = true
+        for (var i = 0; i < data.length; ++i) {
+          var vererr;
+          if (i % 2) {
+            if (data[i].version !== 1) {
+              vererr = "Unsupported graph version: " + data[i].version;
+              console.log(vererr); //silent fail
+            } else {
+              data[i].batadv.links.forEach(rearrangeLinks);
+              dataGraph.batadv.nodes = dataGraph.batadv.nodes.concat(data[i].batadv.nodes);
+              dataGraph.batadv.links = dataGraph.batadv.links.concat(data[i].batadv.links);
+              dataGraph.timestamp = data[i].timestamp;
+            }
+          } else if (data[i].version !== 2) {
+            vererr = "Unsupported nodes version: " + data[i].version;
+            console.log(vererr); //silent fail
+          } else {
+            data[i].nodes.forEach(fillData);
+            dataNodes.timestamp = data[i].timestamp;
           }
         }
-      })
 
-      graph.links.forEach( function (d) {
-        d.source = graph.nodes[d.source]
+        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;
+          }
+        }
 
-        if (graph.nodes[d.target].node)
-          d.target = graph.nodes[d.target]
-        else
-          d.target = undefined
-      })
+        var nodes = dataNodes.nodes.filter(function (d) {
+          return "firstseen" in d && "lastseen" in d;
+        });
 
-      var links = graph.links.filter( function (d) {
-        return d.target !== undefined
-      })
+        nodes.forEach(function (node) {
+          node.firstseen = moment.utc(node.firstseen).local();
+          node.lastseen = moment.utc(node.lastseen).local();
+        });
 
-      links.forEach( function (d) {
-        var unknown = (d.source.node === undefined)
-        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("-")
+        var now = moment();
+        var age = moment(now).subtract(config.maxAge, "days");
 
-        if (unknown ||
-            !d.source.node.nodeinfo.location ||
-            !d.target.node.nodeinfo.location ||
+        var newnodes = limit("firstseen", age, sortByKey("firstseen", nodes).filter(online));
+        var lostnodes = limit("lastseen", age, sortByKey("lastseen", nodes).filter(offline));
+
+        var graphnodes = {};
+
+        dataNodes.nodes.forEach(function (d) {
+          graphnodes[d.nodeinfo.node_id] = d;
+        });
+
+        var graph = dataGraph.batadv;
+
+        graph.nodes.forEach(function (d) {
+          if (d.node_id in graphnodes) {
+            d.node = graphnodes[d.node_id];
+            if (d.unseen) {
+              d.node.flags.online = true;
+              d.node.flags.unseen = true;
+            }
+          }
+        });
+
+        graph.links.forEach(function (d) {
+          d.source = graph.nodes[d.source];
+
+          if (graph.nodes[d.target].node) {
+            d.target = graph.nodes[d.target];
+          } else {
+            d.target = undefined;
+          }
+        });
+
+        var links = graph.links.filter(function (d) {
+          return d.target !== undefined;
+        });
+
+        links.forEach(function (d) {
+          var unknown = (d.source.node === undefined);
+          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 (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
+            isNaN(d.target.node.nodeinfo.location.longitude)) {
+            return;
+          }
 
-        d.latlngs = []
-        d.latlngs.push(L.latLng(d.source.node.nodeinfo.location.latitude, d.source.node.nodeinfo.location.longitude))
-        d.latlngs.push(L.latLng(d.target.node.nodeinfo.location.latitude, d.target.node.nodeinfo.location.longitude))
+          d.latlngs = [];
+          d.latlngs.push(L.latLng(d.source.node.nodeinfo.location.latitude, d.source.node.nodeinfo.location.longitude));
+          d.latlngs.push(L.latLng(d.target.node.nodeinfo.location.latitude, d.target.node.nodeinfo.location.longitude));
 
-        d.distance = d.latlngs[0].distanceTo(d.latlngs[1])
-      })
+          d.distance = d.latlngs[0].distanceTo(d.latlngs[1]);
+        });
 
-      nodes.forEach( function (d) {
-        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}
-        }
-      })
+        nodes.forEach(function (d) {
+          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) {
-        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
-        }
+        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
+          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.target.node.neighbours.push({ node: d.source.node, link: d, incoming: true })
-        if (!d.isVPN)
-          d.source.node.meshlinks = d.source.node.meshlinks ? d.source.node.meshlinks + 1 : 1
-      })
+          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.target.node.neighbours.push({node: d.source.node, link: d, incoming: true});
+          if (!d.isVPN) {
+            d.source.node.meshlinks = d.source.node.meshlinks ? d.source.node.meshlinks + 1 : 1;
+          }
+        });
 
-      links.sort( function (a, b) {
-        return b.tq - a.tq
-      })
+        links.sort(function (a, b) {
+          return b.tq - a.tq;
+        });
 
-      return { now: now,
-               timestamp: moment.utc(dataNodes.timestamp).local(),
-               nodes: {
-                 all: nodes,
-                 new: newnodes,
-                 lost: lostnodes
-               },
-               graph: {
-                 links: links,
-                 nodes: graph.nodes
-               }
-             }
-    }
+        return {
+          now: now,
+          timestamp: moment.utc(dataNodes.timestamp).local(),
+          nodes: {
+            all: nodes,
+            new: newnodes,
+            lost: lostnodes
+          },
+          graph: {
+            links: links,
+            nodes: graph.nodes
+          }
+        };
+      }
 
-    numeral.language("de")
-    moment.locale("de")
+      numeral.language("de");
+      moment.locale("de");
 
-    var router = new Router()
+      var router = new Router();
 
-    var urls = []
+      var urls = [];
 
-    if (typeof config.dataPath === "string" || config.dataPath instanceof String)
-      config.dataPath = [config.dataPath]
+      if (typeof config.dataPath === "string" || config.dataPath instanceof String) {
+        config.dataPath = [config.dataPath];
+      }
 
-    for (var i in config.dataPath) {
-      urls.push(config.dataPath[i] + "nodes.json")
-      urls.push(config.dataPath[i] + "graph.json")
-    }
+      for (var i in config.dataPath) {
+        urls.push(config.dataPath[i] + "nodes.json");
+        urls.push(config.dataPath[i] + "graph.json");
+      }
 
-    function update() {
-      return Promise.all(urls.map(getJSON))
-                    .then(handleData)
-    }
+      function update() {
+        return Promise.all(urls.map(getJSON))
+          .then(handleData);
+      }
 
-    update()
-      .then(function (d) {
-        var gui = new GUI(config, router)
-        gui.setData(d)
-        router.setData(d)
-        router.start()
+      update()
+        .then(function (d) {
+          var gui = new GUI(config, router);
+          gui.setData(d);
+          router.setData(d);
+          router.start();
 
-        window.setInterval(function () {
-          update().then(function (d) {
-            gui.setData(d)
-            router.setData(d)
-          })
-        }, 60000)
-      })
-      .catch(function (e) {
-        document.body.textContent = e
-        console.log(e)
-      })
-  }
-})
+          window.setInterval(function () {
+            update().then(function (d) {
+              gui.setData(d);
+              router.setData(d);
+            });
+          }, 60000);
+        })
+        .catch(function (e) {
+          document.body.textContent = e;
+          console.log(e);
+        });
+    };
+  });
diff --git a/lib/map.js b/lib/map.js
index aca0097..ea44443 100644
--- a/lib/map.js
+++ b/lib/map.js
@@ -1,74 +1,75 @@
 define(["map/clientlayer", "map/labelslayer",
-        "d3", "leaflet", "moment", "locationmarker", "rbush",
-        "leaflet.label", "leaflet.providers"],
+    "d3", "leaflet", "moment", "locationmarker", "rbush",
+    "leaflet.label", "leaflet.providers"],
   function (ClientLayer, LabelsLayer, d3, L, moment, LocationMarker, rbush) {
-    var options = { worldCopyJump: true,
-                    zoomControl: false
-                  }
+    var options = {
+      worldCopyJump: true,
+      zoomControl: false
+    };
 
     var AddLayerButton = L.Control.extend({
-        options: {
-          position: "bottomright"
-        },
+      options: {
+        position: "bottomright"
+      },
 
-        initialize: function (f, options) {
-          L.Util.setOptions(this, options)
-          this.f = f
-        },
+      initialize: function (f, options) {
+        L.Util.setOptions(this, options);
+        this.f = f;
+      },
 
-        onAdd: function () {
-          var button = L.DomUtil.create("button", "add-layer")
-          button.textContent = "\uF2C7"
+      onAdd: function () {
+        var button = L.DomUtil.create("button", "add-layer");
+        button.textContent = "\uF2C7";
 
-          // L.DomEvent.disableClickPropagation(button)
-          // Click propagation isn't disabled as this causes problems with the
-          // location picking mode; instead propagation is stopped in onClick().
-          L.DomEvent.addListener(button, "click", this.f, this)
+        // L.DomEvent.disableClickPropagation(button)
+        // Click propagation isn't disabled as this causes problems with the
+        // location picking mode; instead propagation is stopped in onClick().
+        L.DomEvent.addListener(button, "click", this.f, this);
 
-          this.button = button
+        this.button = button;
 
-          return button
-        }
-    })
+        return button;
+      }
+    });
 
     var LocateButton = L.Control.extend({
-        options: {
-          position: "bottomright"
-        },
+      options: {
+        position: "bottomright"
+      },
 
-        active: false,
-        button: undefined,
+      active: false,
+      button: undefined,
 
-        initialize: function (f, options) {
-          L.Util.setOptions(this, options)
-          this.f = f
-        },
+      initialize: function (f, options) {
+        L.Util.setOptions(this, options);
+        this.f = f;
+      },
 
-        onAdd: function () {
-          var button = L.DomUtil.create("button", "locate-user")
-          button.textContent = "\uF2E9"
+      onAdd: function () {
+        var button = L.DomUtil.create("button", "locate-user");
+        button.textContent = "\uF2E9";
 
-          L.DomEvent.disableClickPropagation(button)
-          L.DomEvent.addListener(button, "click", this.onClick, this)
+        L.DomEvent.disableClickPropagation(button);
+        L.DomEvent.addListener(button, "click", this.onClick, this);
 
-          this.button = button
+        this.button = button;
 
-          return button
-        },
+        return button;
+      },
 
-        update: function() {
-          this.button.classList.toggle("active", this.active)
-        },
+      update: function () {
+        this.button.classList.toggle("active", this.active);
+      },
 
-        set: function(v) {
-          this.active = v
-          this.update()
-        },
+      set: function (v) {
+        this.active = v;
+        this.update();
+      },
 
-        onClick: function () {
-          this.f(!this.active)
-        }
-    })
+      onClick: function () {
+        this.f(!this.active);
+      }
+    });
 
     var CoordsPickerButton = L.Control.extend({
       options: {
@@ -79,470 +80,547 @@ define(["map/clientlayer", "map/labelslayer",
       button: undefined,
 
       initialize: function (f, options) {
-        L.Util.setOptions(this, options)
-        this.f = f
+        L.Util.setOptions(this, options);
+        this.f = f;
       },
 
       onAdd: function () {
-        var button = L.DomUtil.create("button", "coord-picker")
-        button.textContent = "\uF2A6"
+        var button = L.DomUtil.create("button", "coord-picker");
+        button.textContent = "\uF2A6";
 
         // Click propagation isn't disabled as this causes problems with the
         // location picking mode; instead propagation is stopped in onClick().
-        L.DomEvent.addListener(button, "click", this.onClick, this)
+        L.DomEvent.addListener(button, "click", this.onClick, this);
 
-        this.button = button
+        this.button = button;
 
-        return button
+        return button;
       },
 
-      update: function() {
-        this.button.classList.toggle("active", this.active)
+      update: function () {
+        this.button.classList.toggle("active", this.active);
       },
 
-      set: function(v) {
-        this.active = v
-        this.update()
+      set: function (v) {
+        this.active = v;
+        this.update();
       },
 
       onClick: function (e) {
-        L.DomEvent.stopPropagation(e)
-        this.f(!this.active)
+        L.DomEvent.stopPropagation(e);
+        this.f(!this.active);
       }
 
-    })
+    });
 
     function mkMarker(dict, iconFunc, router) {
       return function (d) {
-        var m = L.circleMarker([d.nodeinfo.location.latitude, d.nodeinfo.location.longitude], iconFunc(d))
+        var m = L.circleMarker([d.nodeinfo.location.latitude, d.nodeinfo.location.longitude], iconFunc(d));
 
         m.resetStyle = function () {
-          m.setStyle(iconFunc(d))
-        }
+          m.setStyle(iconFunc(d));
+        };
 
-        m.on("click", router.node(d))
-        m.bindLabel(d.nodeinfo.hostname)
+        m.on("click", router.node(d));
+        m.bindLabel(d.nodeinfo.hostname);
 
-        dict[d.nodeinfo.node_id] = m
+        dict[d.nodeinfo.node_id] = m;
 
-        return m
-      }
+        return m;
+      };
     }
 
     function addLinksToMap(dict, linkScale, graph, router) {
-      graph = graph.filter( function (d) {
-        return "distance" in d && d.type !== "VPN"
-      })
+      graph = graph.filter(function (d) {
+        return "distance" in d && d.type !== "VPN";
+      });
 
-      var lines = graph.map( function (d) {
-        var opts = { color: d.type === "Kabel" ? "#50B0F0" : linkScale(d.tq).hex(),
-                     weight: 4,
-                     opacity: 0.5,
-                     dashArray: "none"
-                   }
+      var lines = graph.map(function (d) {
+        var opts = {
+          color: d.type === "Kabel" ? "#50B0F0" : linkScale(d.tq).hex(),
+          weight: 4,
+          opacity: 0.5,
+          dashArray: "none"
+        };
 
-        var line = L.polyline(d.latlngs, opts)
+        var line = L.polyline(d.latlngs, opts);
 
         line.resetStyle = function () {
-          line.setStyle(opts)
-        }
+          line.setStyle(opts);
+        };
 
-        line.bindLabel(d.source.node.nodeinfo.hostname + " – " + d.target.node.nodeinfo.hostname + "<br><strong>" + showDistance(d) + " / " + showTq(d) + "</strong>")
-        line.on("click", router.link(d))
+        line.bindLabel(d.source.node.nodeinfo.hostname + " – " + d.target.node.nodeinfo.hostname + "<br><strong>" + showDistance(d) + " / " + showTq(d) + "</strong>");
+        line.on("click", router.link(d));
 
-        dict[d.id] = line
+        dict[d.id] = line;
 
-        return line
-      })
+        return line;
+      });
 
-      return lines
+      return lines;
     }
 
-    var iconOnline  = { color: "#1566A9", fillColor: "#1566A9", radius: 6, fillOpacity: 0.5, opacity: 0.5, weight: 2, className: "stroke-first" }
-    var iconOffline = { color: "#D43E2A", fillColor: "#D43E2A", radius: 3, fillOpacity: 0.5, opacity: 0.5, weight: 1, className: "stroke-first" }
-    var iconLost    = { color: "#D43E2A", fillColor: "#D43E2A", radius: 6, fillOpacity: 0.8, opacity: 0.8, weight: 1, className: "stroke-first" }
-    var iconAlert   = { color: "#D43E2A", fillColor: "#D43E2A", radius: 6, fillOpacity: 0.8, opacity: 0.8, weight: 2, className: "stroke-first node-alert" }
-    var iconNew     = { color: "#1566A9", fillColor: "#93E929", radius: 6, fillOpacity: 1.0, opacity: 0.5, weight: 2 }
+    var iconOnline = {
+      color: "#1566A9",
+      fillColor: "#1566A9",
+      radius: 6,
+      fillOpacity: 0.5,
+      opacity: 0.5,
+      weight: 2,
+      className: "stroke-first"
+    };
+    var iconOffline = {
+      color: "#D43E2A",
+      fillColor: "#D43E2A",
+      radius: 3,
+      fillOpacity: 0.5,
+      opacity: 0.5,
+      weight: 1,
+      className: "stroke-first"
+    };
+    var iconLost = {
+      color: "#D43E2A",
+      fillColor: "#D43E2A",
+      radius: 6,
+      fillOpacity: 0.8,
+      opacity: 0.8,
+      weight: 1,
+      className: "stroke-first"
+    };
+    var iconAlert = {
+      color: "#D43E2A",
+      fillColor: "#D43E2A",
+      radius: 6,
+      fillOpacity: 0.8,
+      opacity: 0.8,
+      weight: 2,
+      className: "stroke-first node-alert"
+    };
+    var iconNew = {color: "#1566A9", fillColor: "#93E929", radius: 6, fillOpacity: 1.0, opacity: 0.5, weight: 2};
 
     return function (config, linkScale, sidebar, router, buttons) {
-      var self = this
-      var barycenter
-      var groupOnline, groupOffline, groupNew, groupLost, groupLines
-      var savedView
+      var self = this;
+      var barycenter;
+      var groupOnline, groupOffline, groupNew, groupLost, groupLines;
+      var savedView;
 
-      var map, userLocation
-      var layerControl
-      var customLayers = {}
-      var baseLayers = {}
+      var map, userLocation;
+      var layerControl;
+      var customLayers = {};
+      var baseLayers = {};
 
       var locateUserButton = new LocateButton(function (d) {
-        if (d)
-          enableTracking()
-        else
-          disableTracking()
-      })
+        if (d) {
+          enableTracking();
+        } else {
+          disableTracking();
+        }
+      });
 
-      var mybuttons = []
+      var mybuttons = [];
 
       function addButton(button) {
-        var el = button.onAdd()
-        mybuttons.push(el)
-        buttons.appendChild(el)
+        var el = button.onAdd();
+        mybuttons.push(el);
+        buttons.appendChild(el);
       }
 
       function clearButtons() {
-        mybuttons.forEach( function (d) {
-          buttons.removeChild(d)
-        })
+        mybuttons.forEach(function (d) {
+          buttons.removeChild(d);
+        });
       }
 
       var showCoordsPickerButton = new CoordsPickerButton(function (d) {
-        if (d)
-          enableCoords()
-        else
-          disableCoords()
-      })
+        if (d) {
+          enableCoords();
+        } else {
+          disableCoords();
+        }
+      });
 
       function saveView() {
-        savedView = {center: map.getCenter(),
-                     zoom: map.getZoom()}
+        savedView = {
+          center: map.getCenter(),
+          zoom: map.getZoom()
+        };
       }
 
       function enableTracking() {
-        map.locate({watch: true,
-                    enableHighAccuracy: true,
-                    setView: true
-                   })
-        locateUserButton.set(true)
+        map.locate({
+          watch: true,
+          enableHighAccuracy: true,
+          setView: true
+        });
+        locateUserButton.set(true);
       }
 
       function disableTracking() {
-        map.stopLocate()
-        locationError()
-        locateUserButton.set(false)
+        map.stopLocate();
+        locationError();
+        locateUserButton.set(false);
       }
 
       function enableCoords() {
-        map.getContainer().classList.add("pick-coordinates")
-        map.on("click", showCoordinates)
-        showCoordsPickerButton.set(true)
+        map.getContainer().classList.add("pick-coordinates");
+        map.on("click", showCoordinates);
+        showCoordsPickerButton.set(true);
       }
 
       function disableCoords() {
-        map.getContainer().classList.remove("pick-coordinates")
-        map.off("click", showCoordinates)
-        showCoordsPickerButton.set(false)
+        map.getContainer().classList.remove("pick-coordinates");
+        map.off("click", showCoordinates);
+        showCoordsPickerButton.set(false);
       }
 
       function showCoordinates(e) {
-        router.gotoLocation(e.latlng)
+        router.gotoLocation(e.latlng);
         // window.prompt("Koordinaten (Lat, Lng)", e.latlng.lat.toFixed(9) + ", " + e.latlng.lng.toFixed(9))
-        disableCoords()
+        disableCoords();
       }
 
       function locationFound(e) {
-        if (!userLocation)
-          userLocation = new LocationMarker(e.latlng).addTo(map)
+        if (!userLocation) {
+          userLocation = new LocationMarker(e.latlng).addTo(map);
+        }
 
-        userLocation.setLatLng(e.latlng)
-        userLocation.setAccuracy(e.accuracy)
+        userLocation.setLatLng(e.latlng);
+        userLocation.setAccuracy(e.accuracy);
       }
 
       function locationError() {
         if (userLocation) {
-          map.removeLayer(userLocation)
-          userLocation = null
+          map.removeLayer(userLocation);
+          userLocation = null;
         }
       }
 
       function addLayer(layerName) {
-        if (layerName in baseLayers)
-          return
+        if (layerName in baseLayers) {
+          return;
+        }
 
-        if (layerName in customLayers)
-          return
+        if (layerName in customLayers) {
+          return;
+        }
 
         try {
-          var layer = L.tileLayer.provider(layerName)
-          layerControl.addBaseLayer(layer, layerName)
-          customLayers[layerName] = layer
+          var layer = L.tileLayer.provider(layerName);
+          layerControl.addBaseLayer(layer, layerName);
+          customLayers[layerName] = layer;
 
-          if (localStorageTest())
-            localStorage.setItem("map/customLayers", JSON.stringify(Object.keys(customLayers)))
+          if (localStorageTest()) {
+            localStorage.setItem("map/customLayers", JSON.stringify(Object.keys(customLayers)));
+          }
         } catch (e) {
-          return
+
         }
       }
 
       function contextMenuGotoLocation(e) {
-        router.gotoLocation(e.latlng)
+        router.gotoLocation(e.latlng);
       }
 
-      var el = document.createElement("div")
-      el.classList.add("map")
+      var el = document.createElement("div");
+      el.classList.add("map");
 
-      map = L.map(el, options)
+      map = L.map(el, options);
 
-      var layers = config.mapLayers.map( function (d) {
+      var layers = config.mapLayers.map(function (d) {
         return {
           "name": d.name,
           "layer": "url" in d ? L.tileLayer(d.url, d.config) : L.tileLayer.provider(d.name)
-        }
-      })
+        };
+      });
 
-      layers[0].layer.addTo(map)
+      layers[0].layer.addTo(map);
 
-      layers.forEach( function (d) {
-        baseLayers[d.name] = d.layer
-      })
+      layers.forEach(function (d) {
+        baseLayers[d.name] = d.layer;
+      });
 
-      map.on("locationfound", locationFound)
-      map.on("locationerror", locationError)
-      map.on("dragend", saveView)
-      map.on("contextmenu", contextMenuGotoLocation)
+      map.on("locationfound", locationFound);
+      map.on("locationerror", locationError);
+      map.on("dragend", saveView);
+      map.on("contextmenu", contextMenuGotoLocation);
 
-      addButton(locateUserButton)
-      addButton(showCoordsPickerButton)
+      addButton(locateUserButton);
+      addButton(showCoordsPickerButton);
 
       addButton(new AddLayerButton(function () {
         /*eslint no-alert:0*/
-        var layerName = prompt("Leaflet Provider:")
-        addLayer(layerName)
-      }))
+        var layerName = prompt("Leaflet Provider:");
+        addLayer(layerName);
+      }));
 
-      layerControl = L.control.layers(baseLayers, [], {position: "bottomright"})
-      layerControl.addTo(map)
+      layerControl = L.control.layers(baseLayers, [], {position: "bottomright"});
+      layerControl.addTo(map);
 
       if (localStorageTest()) {
-        var d = JSON.parse(localStorage.getItem("map/customLayers"))
-
-        if (d)
-          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
+        var d = JSON.parse(localStorage.getItem("map/customLayers"));
 
         if (d) {
-          map.removeLayer(layers[0].layer)
-          map.addLayer(d)
+          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})
-      clientLayer.addTo(map)
-      clientLayer.setZIndex(5)
+      var clientLayer = new ClientLayer({minZoom: 15});
+      clientLayer.addTo(map);
+      clientLayer.setZIndex(5);
 
-      var labelsLayer = new LabelsLayer({})
-      labelsLayer.addTo(map)
-      labelsLayer.setZIndex(6)
+      var labelsLayer = new LabelsLayer({});
+      labelsLayer.addTo(map);
+      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}))
-      })
+      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 linkDict = {}
-      var highlight
+      var nodeDict = {};
+      var linkDict = {};
+      var highlight;
 
       function resetMarkerStyles(nodes, links) {
-        Object.keys(nodes).forEach( function (d) {
-          nodes[d].resetStyle()
-        })
+        Object.keys(nodes).forEach(function (d) {
+          nodes[d].resetStyle();
+        });
 
-        Object.keys(links).forEach( function (d) {
-          links[d].resetStyle()
-        })
+        Object.keys(links).forEach(function (d) {
+          links[d].resetStyle();
+        });
       }
 
       function setView(bounds) {
-        map.fitBounds(bounds, {paddingTopLeft: [sidebar(), 0]})
+        map.fitBounds(bounds, {paddingTopLeft: [sidebar(), 0]});
       }
 
       function resetZoom() {
-        if (barycenter)
-          setView(barycenter.getBounds())
+        if (barycenter) {
+          setView(barycenter.getBounds());
+        }
       }
 
       function goto(m) {
-        var bounds
+        var bounds;
 
-        if ("getBounds" in m)
-          bounds = m.getBounds()
-        else
-          bounds = L.latLngBounds([m.getLatLng()])
+        if ("getBounds" in m) {
+          bounds = m.getBounds();
+        } else {
+          bounds = L.latLngBounds([m.getLatLng()]);
+        }
 
-        setView(bounds)
+        setView(bounds);
 
-        return m
+        return m;
       }
 
       function updateView(nopanzoom) {
-        resetMarkerStyles(nodeDict, linkDict)
-        var m
+        resetMarkerStyles(nodeDict, linkDict);
+        var m;
 
-        if (highlight !== undefined)
+        if (highlight !== undefined) {
           if (highlight.type === "node") {
-            m = nodeDict[highlight.o.nodeinfo.node_id]
+            m = nodeDict[highlight.o.nodeinfo.node_id];
 
-            if (m)
-              m.setStyle({ color: "orange", weight: 20, fillOpacity: 1, opacity: 0.7, className: "stroke-first" })
+            if (m) {
+              m.setStyle({color: "orange", weight: 20, fillOpacity: 1, opacity: 0.7, className: "stroke-first"});
+            }
           } else if (highlight.type === "link") {
-            m = linkDict[highlight.o.id]
+            m = linkDict[highlight.o.id];
 
-            if (m)
-              m.setStyle({ weight: 7, opacity: 1, dashArray: "10, 10" })
+            if (m) {
+              m.setStyle({weight: 7, opacity: 1, dashArray: "10, 10"});
+            }
           }
+        }
 
-        if (!nopanzoom)
-          if (m)
-            goto(m)
-          else if (savedView)
-            map.setView(savedView.center, savedView.zoom)
-          else
-            resetZoom()
+        if (!nopanzoom) {
+          if (m) {
+            goto(m);
+          } else if (savedView) {
+            map.setView(savedView.center, savedView.zoom);
+          } else {
+            resetZoom();
+          }
+        }
       }
 
       function calcBarycenter(nodes) {
-        nodes = nodes.map(function (d) { return d.nodeinfo.location })
+        nodes = nodes.map(function (d) {
+          return d.nodeinfo.location;
+        });
 
-        if (nodes.length === 0)
-          return undefined
+        if (nodes.length === 0) {
+          return undefined;
+        }
 
-        var lats = nodes.map(function (d) { return d.latitude })
-        var lngs = nodes.map(function (d) { return d.longitude })
+        var lats = nodes.map(function (d) {
+          return d.latitude;
+        });
+        var lngs = nodes.map(function (d) {
+          return d.longitude;
+        });
 
-        var barycenter = L.latLng(d3.median(lats), d3.median(lngs))
-        var barycenterDev = [d3.deviation(lats), d3.deviation(lngs)]
+        var barycenter = L.latLng(d3.median(lats), d3.median(lngs));
+        var barycenterDev = [d3.deviation(lats), d3.deviation(lngs)];
 
-        if (barycenterDev[0] === undefined)
-          barycenterDev[0] = 0
+        if (barycenterDev[0] === undefined) {
+          barycenterDev[0] = 0;
+        }
 
-        if (barycenterDev[1] === undefined)
-          barycenterDev[1] = 0
+        if (barycenterDev[1] === undefined) {
+          barycenterDev[1] = 0;
+        }
 
         var barycenterCircle = L.latLng(barycenter.lat + barycenterDev[0],
-                                        barycenter.lng + barycenterDev[1])
+          barycenter.lng + barycenterDev[1]);
 
-        var r = barycenter.distanceTo(barycenterCircle)
+        var r = barycenter.distanceTo(barycenterCircle);
 
-        return L.circle(barycenter, r * config.mapSigmaScale)
+        return L.circle(barycenter, r * config.mapSigmaScale);
       }
 
       function mapRTree(d) {
-        var o = [ d.nodeinfo.location.latitude, d.nodeinfo.location.longitude,
-                  d.nodeinfo.location.latitude, d.nodeinfo.location.longitude]
+        var o = [d.nodeinfo.location.latitude, d.nodeinfo.location.longitude,
+          d.nodeinfo.location.latitude, d.nodeinfo.location.longitude];
 
-        o.node = d
+        o.node = d;
 
-        return o
+        return o;
       }
 
       self.setData = function (data) {
-        nodeDict = {}
-        linkDict = {}
+        nodeDict = {};
+        linkDict = {};
 
-        if (groupOffline)
-          groupOffline.clearLayers()
+        if (groupOffline) {
+          groupOffline.clearLayers();
+        }
 
-        if (groupOnline)
-          groupOnline.clearLayers()
+        if (groupOnline) {
+          groupOnline.clearLayers();
+        }
 
-        if (groupNew)
-          groupNew.clearLayers()
+        if (groupNew) {
+          groupNew.clearLayers();
+        }
 
-        if (groupLost)
-          groupLost.clearLayers()
+        if (groupLost) {
+          groupLost.clearLayers();
+        }
 
-        if (groupLines)
-          groupLines.clearLayers()
+        if (groupLines) {
+          groupLines.clearLayers();
+        }
 
-        var lines = addLinksToMap(linkDict, linkScale, data.graph.links, router)
-        groupLines = L.featureGroup(lines).addTo(map)
+        var lines = addLinksToMap(linkDict, linkScale, data.graph.links, router);
+        groupLines = L.featureGroup(lines).addTo(map);
 
-        if (typeof config.fixedCenter === "undefined")
-          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)
+        if (typeof config.fixedCenter === "undefined") {
+          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 nodesOffline = subtract(data.nodes.all.filter(offline), data.nodes.lost)
+        var nodesOnline = subtract(data.nodes.all.filter(online), data.nodes.new);
+        var nodesOffline = subtract(data.nodes.all.filter(offline), data.nodes.lost);
 
         var markersOnline = nodesOnline.filter(has_location)
-          .map(mkMarker(nodeDict, function () { return iconOnline }, router))
+          .map(mkMarker(nodeDict, function () {
+            return iconOnline;
+          }, router));
 
         var markersOffline = nodesOffline.filter(has_location)
-          .map(mkMarker(nodeDict, function () { return iconOffline }, router))
+          .map(mkMarker(nodeDict, function () {
+            return iconOffline;
+          }, router));
 
         var markersNew = data.nodes.new.filter(has_location)
-          .map(mkMarker(nodeDict, function () { return iconNew }, router))
+          .map(mkMarker(nodeDict, function () {
+            return iconNew;
+          }, router));
 
         var markersLost = data.nodes.lost.filter(has_location)
           .map(mkMarker(nodeDict, function (d) {
-            if (d.lastseen.isAfter(moment(data.now).subtract(3, "days")))
-              return iconAlert
+            if (d.lastseen.isAfter(moment(data.now).subtract(3, "days"))) {
+              return iconAlert;
+            }
 
-            return iconLost
-          }, router))
+            return iconLost;
+          }, router));
 
-        groupOffline = L.featureGroup(markersOffline).addTo(map)
-        groupOnline = L.featureGroup(markersOnline).addTo(map)
-        groupLost = L.featureGroup(markersLost).addTo(map)
-        groupNew = L.featureGroup(markersNew).addTo(map)
+        groupOffline = L.featureGroup(markersOffline).addTo(map);
+        groupOnline = L.featureGroup(markersOnline).addTo(map);
+        groupLost = L.featureGroup(markersLost).addTo(map);
+        groupNew = L.featureGroup(markersNew).addTo(map);
 
-        var rtreeOnlineAll = rbush(9)
+        var rtreeOnlineAll = rbush(9);
 
-        rtreeOnlineAll.load(data.nodes.all.filter(online).filter(has_location).map(mapRTree))
+        rtreeOnlineAll.load(data.nodes.all.filter(online).filter(has_location).map(mapRTree));
 
-        clientLayer.setData(rtreeOnlineAll)
-        labelsLayer.setData({online: nodesOnline.filter(has_location),
-                             offline: nodesOffline.filter(has_location),
-                             new: data.nodes.new.filter(has_location),
-                             lost: data.nodes.lost.filter(has_location)
-                            })
+        clientLayer.setData(rtreeOnlineAll);
+        labelsLayer.setData({
+          online: nodesOnline.filter(has_location),
+          offline: nodesOffline.filter(has_location),
+          new: data.nodes.new.filter(has_location),
+          lost: data.nodes.lost.filter(has_location)
+        });
 
-        updateView(true)
-      }
+        updateView(true);
+      };
 
       self.resetView = function () {
-        disableTracking()
-        highlight = undefined
-        updateView()
-      }
+        disableTracking();
+        highlight = undefined;
+        updateView();
+      };
 
       self.gotoNode = function (d) {
-        disableTracking()
-        highlight = {type: "node", o: d}
-        updateView()
-      }
+        disableTracking();
+        highlight = {type: "node", o: d};
+        updateView();
+      };
 
       self.gotoLink = function (d) {
-        disableTracking()
-        highlight = {type: "link", o: d}
-        updateView()
-      }
+        disableTracking();
+        highlight = {type: "link", o: d};
+        updateView();
+      };
 
       self.gotoLocation = function () {
         //ignore
-      }
+      };
 
       self.destroy = function () {
-        clearButtons()
-        map.remove()
+        clearButtons();
+        map.remove();
 
-        if (el.parentNode)
-          el.parentNode.removeChild(el)
-      }
+        if (el.parentNode) {
+          el.parentNode.removeChild(el);
+        }
+      };
 
       self.render = function (d) {
-        d.appendChild(el)
-        map.invalidateSize()
-      }
+        d.appendChild(el);
+        map.invalidateSize();
+      };
 
-      return self
-    }
-})
+      return self;
+    };
+  });
diff --git a/lib/map/clientlayer.js b/lib/map/clientlayer.js
index c3fb439..d6ee4fc 100644
--- a/lib/map/clientlayer.js
+++ b/lib/map/clientlayer.js
@@ -1,76 +1,79 @@
 define(["leaflet", "jshashes"],
   function (L, jsHashes) {
-    var MD5 = new jsHashes.MD5()
+    var MD5 = new jsHashes.MD5();
 
     return L.TileLayer.Canvas.extend({
       setData: function (d) {
-        this.data = d
+        this.data = d;
 
         //pre-calculate start angles
         this.data.all().forEach(function (d) {
-          var hash = MD5.hex(d.node.nodeinfo.node_id)
-          d.startAngle = (parseInt(hash.substr(0, 2), 16) / 255) * 2 * Math.PI
-        })
-        this.redraw()
+          var hash = MD5.hex(d.node.nodeinfo.node_id);
+          d.startAngle = (parseInt(hash.substr(0, 2), 16) / 255) * 2 * Math.PI;
+        });
+        this.redraw();
       },
       drawTile: function (canvas, tilePoint) {
         function getTileBBox(s, map, tileSize, margin) {
-          var tl = map.unproject([s.x - margin, s.y - margin])
-          var br = map.unproject([s.x + margin + tileSize, s.y + margin + tileSize])
+          var tl = map.unproject([s.x - margin, s.y - margin]);
+          var br = map.unproject([s.x + margin + tileSize, s.y + margin + tileSize]);
 
-          return [br.lat, tl.lng, tl.lat, br.lng]
+          return [br.lat, tl.lng, tl.lat, br.lng];
         }
 
-        if (!this.data)
-          return
+        if (!this.data) {
+          return;
+        }
 
-        var tileSize = this.options.tileSize
-        var s = tilePoint.multiplyBy(tileSize)
-        var map = this._map
+        var tileSize = this.options.tileSize;
+        var s = tilePoint.multiplyBy(tileSize);
+        var map = this._map;
 
-        var margin = 50
-        var bbox = getTileBBox(s, map, tileSize, margin)
+        var margin = 50;
+        var bbox = getTileBBox(s, map, tileSize, margin);
 
-        var nodes = this.data.search(bbox)
+        var nodes = this.data.search(bbox);
 
-        if (nodes.length === 0)
-          return
+        if (nodes.length === 0) {
+          return;
+        }
 
-        var ctx = canvas.getContext("2d")
+        var ctx = canvas.getContext("2d");
 
-        var radius = 3
-        var a = 1.2
-        var startDistance = 12
+        var radius = 3;
+        var a = 1.2;
+        var startDistance = 12;
 
-        ctx.beginPath()
+        ctx.beginPath();
         nodes.forEach(function (d) {
-          var p = map.project([d.node.nodeinfo.location.latitude, d.node.nodeinfo.location.longitude])
-          var clients = d.node.statistics.clients
+          var p = map.project([d.node.nodeinfo.location.latitude, d.node.nodeinfo.location.longitude]);
+          var clients = d.node.statistics.clients;
 
-          if (clients === 0)
-            return
+          if (clients === 0) {
+            return;
+          }
 
-          p.x -= s.x
-          p.y -= s.y
+          p.x -= s.x;
+          p.y -= s.y;
 
           for (var orbit = 0, i = 0; i < clients; orbit++) {
-            var distance = startDistance + orbit * 2 * radius * a
-            var n = Math.floor((Math.PI * distance) / (a * radius))
-            var delta = clients - i
+            var distance = startDistance + orbit * 2 * radius * a;
+            var n = Math.floor((Math.PI * distance) / (a * radius));
+            var delta = clients - i;
 
             for (var j = 0; j < Math.min(delta, n); i++, j++) {
-              var angle = 2 * Math.PI / n * j
-              var x = p.x + distance * Math.cos(angle + d.startAngle)
-              var y = p.y + distance * Math.sin(angle + d.startAngle)
+              var angle = 2 * Math.PI / n * j;
+              var x = p.x + distance * Math.cos(angle + d.startAngle);
+              var y = p.y + distance * Math.sin(angle + d.startAngle);
 
-              ctx.moveTo(x, y)
-              ctx.arc(x, y, radius, 0, 2 * Math.PI)
+              ctx.moveTo(x, y);
+              ctx.arc(x, y, radius, 0, 2 * Math.PI);
             }
           }
-        })
+        });
 
-        ctx.fillStyle = "rgba(220, 0, 103, 0.7)"
-        ctx.fill()
+        ctx.fillStyle = "rgba(220, 0, 103, 0.7)";
+        ctx.fill();
       }
-    })
-})
+    });
+  });
diff --git a/lib/map/labelslayer.js b/lib/map/labelslayer.js
index 2912c10..e616877 100644
--- a/lib/map/labelslayer.js
+++ b/lib/map/labelslayer.js
@@ -1,88 +1,93 @@
 define(["leaflet", "rbush"],
   function (L, rbush) {
-    var labelLocations = [["left",   "middle",      0 / 8],
-                          ["center", "top",         6 / 8],
-                          ["right",  "middle",      4 / 8],
-                          ["left",   "top",         7 / 8],
-                          ["left",   "ideographic", 1 / 8],
-                          ["right",  "top",         5 / 8],
-                          ["center", "ideographic", 2 / 8],
-                          ["right",  "ideographic", 3 / 8]]
+    var labelLocations = [["left", "middle", 0 / 8],
+      ["center", "top", 6 / 8],
+      ["right", "middle", 4 / 8],
+      ["left", "top", 7 / 8],
+      ["left", "ideographic", 1 / 8],
+      ["right", "top", 5 / 8],
+      ["center", "ideographic", 2 / 8],
+      ["right", "ideographic", 3 / 8]];
 
-    var fontFamily = "Roboto"
-    var nodeRadius = 4
+    var fontFamily = "Roboto";
+    var nodeRadius = 4;
 
-    var ctx = document.createElement("canvas").getContext("2d")
+    var ctx = document.createElement("canvas").getContext("2d");
 
     function measureText(font, text) {
-      ctx.font = font
-      return ctx.measureText(text)
+      ctx.font = font;
+      return ctx.measureText(text);
     }
 
     function mapRTree(d) {
-      var o = [d.position.lat, d.position.lng, d.position.lat, d.position.lng]
+      var o = [d.position.lat, d.position.lng, d.position.lat, d.position.lng];
 
-      o.label = d
+      o.label = d;
 
-      return o
+      return o;
     }
 
     function prepareLabel(fillStyle, fontSize, offset, stroke, minZoom) {
       return function (d) {
-        var font = fontSize + "px " + fontFamily
-        return { position: L.latLng(d.nodeinfo.location.latitude, d.nodeinfo.location.longitude),
-                 label: d.nodeinfo.hostname,
-                 offset: offset,
-                 fillStyle: fillStyle,
-                 height: fontSize * 1.2,
-                 font: font,
-                 stroke: stroke,
-                 minZoom: minZoom,
-                 width: measureText(font, d.nodeinfo.hostname).width
-               }
-      }
+        var font = fontSize + "px " + fontFamily;
+        return {
+          position: L.latLng(d.nodeinfo.location.latitude, d.nodeinfo.location.longitude),
+          label: d.nodeinfo.hostname,
+          offset: offset,
+          fillStyle: fillStyle,
+          height: fontSize * 1.2,
+          font: font,
+          stroke: stroke,
+          minZoom: minZoom,
+          width: measureText(font, d.nodeinfo.hostname).width
+        };
+      };
     }
 
     function calcOffset(offset, loc) {
-      return [ offset * Math.cos(loc[2] * 2 * Math.PI),
-              -offset * Math.sin(loc[2] * 2 * Math.PI)]
+      return [offset * Math.cos(loc[2] * 2 * Math.PI),
+        -offset * Math.sin(loc[2] * 2 * Math.PI)];
     }
 
     function labelRect(p, offset, anchor, label, minZoom, maxZoom, z) {
-      var margin = 1 + 1.41 * (1 - (z - minZoom) / (maxZoom - minZoom))
+      var margin = 1 + 1.41 * (1 - (z - minZoom) / (maxZoom - minZoom));
 
-      var width = label.width * margin
-      var height = label.height * margin
+      var width = label.width * margin;
+      var height = label.height * margin;
 
-      var dx = { left: 0,
-                 right: -width,
-                 center: -width /  2
-               }
+      var dx = {
+        left: 0,
+        right: -width,
+        center: -width / 2
+      };
 
-      var dy = { top: 0,
-                 ideographic: -height,
-                 middle: -height / 2
-               }
+      var dy = {
+        top: 0,
+        ideographic: -height,
+        middle: -height / 2
+      };
 
-      var x = p.x + offset[0] + dx[anchor[0]]
-      var y = p.y + offset[1] + dy[anchor[1]]
+      var x = p.x + offset[0] + dx[anchor[0]];
+      var y = p.y + offset[1] + dy[anchor[1]];
 
-      return [x, y, x + width, y + height]
+      return [x, y, x + width, y + height];
     }
 
     var c = L.TileLayer.Canvas.extend({
       onAdd: function (map) {
-        L.TileLayer.Canvas.prototype.onAdd.call(this, map)
-        if (this.data)
-          this.prepareLabels()
+        L.TileLayer.Canvas.prototype.onAdd.call(this, map);
+        if (this.data) {
+          this.prepareLabels();
+        }
       },
       setData: function (d) {
-        this.data = d
-        if (this._map)
-          this.prepareLabels()
+        this.data = d;
+        if (this._map) {
+          this.prepareLabels();
+        }
       },
       prepareLabels: function () {
-        var d = this.data
+        var d = this.data;
 
         // label:
         // - position (WGS84 coords)
@@ -92,137 +97,144 @@ define(["leaflet", "rbush"],
         // - label (string)
         // - color (string)
 
-        var labelsOnline = d.online.map(prepareLabel("rgba(0, 0, 0, 0.9)", 10, 8, true, 13))
-        var labelsOffline = d.offline.map(prepareLabel("rgba(212, 62, 42, 0.9)", 9, 5, false, 16))
-        var labelsNew = d.new.map(prepareLabel("rgba(48, 99, 20, 0.9)", 11, 8, true, 0))
-        var labelsLost = d.lost.map(prepareLabel("rgba(212, 62, 42, 0.9)", 11, 8, true, 0))
+        var labelsOnline = d.online.map(prepareLabel("rgba(0, 0, 0, 0.9)", 10, 8, true, 13));
+        var labelsOffline = d.offline.map(prepareLabel("rgba(212, 62, 42, 0.9)", 9, 5, false, 16));
+        var labelsNew = d.new.map(prepareLabel("rgba(48, 99, 20, 0.9)", 11, 8, true, 0));
+        var labelsLost = d.lost.map(prepareLabel("rgba(212, 62, 42, 0.9)", 11, 8, true, 0));
 
         var labels = []
-                     .concat(labelsNew)
-                     .concat(labelsLost)
-                     .concat(labelsOnline)
-                     .concat(labelsOffline)
+          .concat(labelsNew)
+          .concat(labelsLost)
+          .concat(labelsOnline)
+          .concat(labelsOffline);
 
-        var minZoom = this.options.minZoom
-        var maxZoom = this.options.maxZoom
+        var minZoom = this.options.minZoom;
+        var maxZoom = this.options.maxZoom;
 
-        var trees = []
+        var trees = [];
 
-        var map = this._map
+        var map = this._map;
 
         function nodeToRect(z) {
           return function (d) {
-            var p = map.project(d.position, z)
+            var p = map.project(d.position, z);
             return [p.x - nodeRadius, p.y - nodeRadius,
-                    p.x + nodeRadius, p.y + nodeRadius]
-          }
+              p.x + nodeRadius, p.y + nodeRadius];
+          };
         }
 
         for (var z = minZoom; z <= maxZoom; z++) {
-          trees[z] = rbush(9)
-          trees[z].load(labels.map(nodeToRect(z)))
+          trees[z] = rbush(9);
+          trees[z].load(labels.map(nodeToRect(z)));
         }
 
         labels = labels.map(function (d) {
           var best = labelLocations.map(function (loc) {
-            var offset = calcOffset(d.offset, loc)
-            var z
+            var offset = calcOffset(d.offset, loc);
+            var z;
 
             for (z = maxZoom; z >= d.minZoom; z--) {
-              var p = map.project(d.position, z)
-              var rect = labelRect(p, offset, loc, d, minZoom, maxZoom, z)
-              var candidates = trees[z].search(rect)
+              var p = map.project(d.position, z);
+              var rect = labelRect(p, offset, loc, d, minZoom, maxZoom, z);
+              var candidates = trees[z].search(rect);
 
-              if (candidates.length > 0)
-                break
+              if (candidates.length > 0) {
+                break;
+              }
             }
 
-            return {loc: loc, z: z + 1}
+            return {loc: loc, z: z + 1};
           }).filter(function (d) {
-            return d.z <= maxZoom
+            return d.z <= maxZoom;
           }).sort(function (a, b) {
-            return a.z - b.z
-          })[0]
+            return a.z - b.z;
+          })[0];
 
           if (best !== undefined) {
-            d.offset = calcOffset(d.offset, best.loc)
-            d.minZoom = best.z
-            d.anchor = best.loc
+            d.offset = calcOffset(d.offset, best.loc);
+            d.minZoom = best.z;
+            d.anchor = best.loc;
 
             for (var z = maxZoom; z >= best.z; z--) {
-              var p = map.project(d.position, z)
-              var rect = labelRect(p, d.offset, best.loc, d, minZoom, maxZoom, z)
-              trees[z].insert(rect)
+              var p = map.project(d.position, z);
+              var rect = labelRect(p, d.offset, best.loc, d, minZoom, maxZoom, z);
+              trees[z].insert(rect);
             }
 
-            return d
-          } else
-            return undefined
-        }).filter(function (d) { return d !== undefined })
+            return d;
+          } else {
+            return undefined;
+          }
+        }).filter(function (d) {
+          return d !== undefined;
+        });
 
-        this.margin = 16
+        this.margin = 16;
 
-        if (labels.length > 0)
+        if (labels.length > 0) {
           this.margin += labels.map(function (d) {
-            return d.width
-          }).sort().reverse()[0]
+            return d.width;
+          }).sort().reverse()[0];
+        }
 
-        this.labels = rbush(9)
-        this.labels.load(labels.map(mapRTree))
+        this.labels = rbush(9);
+        this.labels.load(labels.map(mapRTree));
 
-        this.redraw()
+        this.redraw();
       },
       drawTile: function (canvas, tilePoint, zoom) {
         function getTileBBox(s, map, tileSize, margin) {
-          var tl = map.unproject([s.x - margin, s.y - margin])
-          var br = map.unproject([s.x + margin + tileSize, s.y + margin + tileSize])
+          var tl = map.unproject([s.x - margin, s.y - margin]);
+          var br = map.unproject([s.x + margin + tileSize, s.y + margin + tileSize]);
 
-          return [br.lat, tl.lng, tl.lat, br.lng]
+          return [br.lat, tl.lng, tl.lat, br.lng];
         }
 
-        if (!this.labels)
-          return
+        if (!this.labels) {
+          return;
+        }
 
-        var tileSize = this.options.tileSize
-        var s = tilePoint.multiplyBy(tileSize)
-        var map = this._map
+        var tileSize = this.options.tileSize;
+        var s = tilePoint.multiplyBy(tileSize);
+        var map = this._map;
 
         function projectNodes(d) {
-          var p = map.project(d.label.position)
+          var p = map.project(d.label.position);
 
-          p.x -= s.x
-          p.y -= s.y
+          p.x -= s.x;
+          p.y -= s.y;
 
-          return {p: p, label: d.label}
+          return {p: p, label: d.label};
         }
 
-        var bbox = getTileBBox(s, map, tileSize, this.margin)
+        var bbox = getTileBBox(s, map, tileSize, this.margin);
 
-        var labels = this.labels.search(bbox).map(projectNodes)
+        var labels = this.labels.search(bbox).map(projectNodes);
 
-        var ctx = canvas.getContext("2d")
+        var ctx = canvas.getContext("2d");
 
-        ctx.lineWidth = 5
-        ctx.strokeStyle = "rgba(255, 255, 255, 0.8)"
-        ctx.miterLimit = 2
+        ctx.lineWidth = 5;
+        ctx.strokeStyle = "rgba(255, 255, 255, 0.8)";
+        ctx.miterLimit = 2;
 
         function drawLabel(d) {
-          ctx.font = d.label.font
-          ctx.textAlign = d.label.anchor[0]
-          ctx.textBaseline = d.label.anchor[1]
-          ctx.fillStyle = d.label.fillStyle
+          ctx.font = d.label.font;
+          ctx.textAlign = d.label.anchor[0];
+          ctx.textBaseline = d.label.anchor[1];
+          ctx.fillStyle = d.label.fillStyle;
 
-          if (d.label.stroke)
-            ctx.strokeText(d.label.label, d.p.x + d.label.offset[0], d.p.y + d.label.offset[1])
+          if (d.label.stroke) {
+            ctx.strokeText(d.label.label, d.p.x + d.label.offset[0], d.p.y + d.label.offset[1]);
+          }
 
-          ctx.fillText(d.label.label, d.p.x + d.label.offset[0], d.p.y + d.label.offset[1])
+          ctx.fillText(d.label.label, d.p.x + d.label.offset[0], d.p.y + d.label.offset[1]);
         }
 
         labels.filter(function (d) {
-          return zoom >= d.label.minZoom
-        }).forEach(drawLabel)
+          return zoom >= d.label.minZoom;
+        }).forEach(drawLabel);
       }
-    })
+    });
 
-    return c
-})
+    return c;
+  });
diff --git a/lib/meshstats.js b/lib/meshstats.js
index 9d50044..0abd3cc 100644
--- a/lib/meshstats.js
+++ b/lib/meshstats.js
@@ -1,55 +1,61 @@
 define(function () {
   return function (config) {
-    var self = this
-    var stats, timestamp
+    var self = this;
+    var stats, timestamp;
 
     self.setData = function (d) {
-      var totalNodes = sum(d.nodes.all.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 totalLostNodes = sum(d.nodes.lost.map(one))
-      var totalClients = sum(d.nodes.all.filter(online).map( function (d) {
-        return d.statistics.clients ? d.statistics.clients : 0
-      }))
-      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
-      })))).map(function(d) {
-        return (typeof d === "string") ? 1 : 0
-      }))
+      var totalNodes = sum(d.nodes.all.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 totalLostNodes = sum(d.nodes.lost.map(one));
+      var totalClients = sum(d.nodes.all.filter(online).map(function (d) {
+        return d.statistics.clients ? d.statistics.clients : 0;
+      }));
+      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;
+      })))).map(function (d) {
+        return (typeof d === "string") ? 1 : 0;
+      }));
 
-      var nodetext = [{ count: totalOnlineNodes, label: "online" },
-                      { count: totalOfflineNodes, label: "offline" },
-                      { count: totalNewNodes, label: "neu" },
-                      { count: totalLostNodes, label: "verschwunden" }
-                     ].filter( function (d) { return d.count > 0 } )
-                      .map( function (d) { return [d.count, d.label].join(" ") } )
-                      .join(", ")
+      var nodetext = [{count: totalOnlineNodes, label: "online"},
+        {count: totalOfflineNodes, label: "offline"},
+        {count: totalNewNodes, label: "neu"},
+        {count: totalLostNodes, label: "verschwunden"}
+      ].filter(function (d) {
+        return d.count > 0;
+      })
+        .map(function (d) {
+          return [d.count, d.label].join(" ");
+        })
+        .join(", ");
 
       stats.textContent = totalNodes + " Knoten " +
-                          "(" + nodetext + "), " +
-                          totalClients + " Client" + ( totalClients === 1 ? ", " : "s, " ) +
-                          totalGateways + " Gateways"
+        "(" + nodetext + "), " +
+        totalClients + " Client" + ( totalClients === 1 ? ", " : "s, " ) +
+        totalGateways + " Gateways";
 
-      timestamp.textContent = "Diese Daten sind von " + d.timestamp.format("LLLL") + "."
-    }
+      timestamp.textContent = "Diese Daten sind von " + d.timestamp.format("LLLL") + ".";
+    };
 
     self.render = function (el) {
-      var h2 = document.createElement("h2")
-      h2.textContent = config.siteName
-      el.appendChild(h2)
+      var h2 = document.createElement("h2");
+      h2.textContent = config.siteName;
+      el.appendChild(h2);
 
-      var p = document.createElement("p")
-      el.appendChild(p)
-      stats = document.createTextNode("")
-      p.appendChild(stats)
-      p.appendChild(document.createElement("br"))
-      timestamp = document.createTextNode("")
-      p.appendChild(timestamp)
-    }
+      var p = document.createElement("p");
+      el.appendChild(p);
+      stats = document.createTextNode("");
+      p.appendChild(stats);
+      p.appendChild(document.createElement("br"));
+      timestamp = document.createTextNode("");
+      p.appendChild(timestamp);
+    };
 
-    return self
-  }
-})
+    return self;
+  };
+});
diff --git a/lib/nodelist.js b/lib/nodelist.js
index 2130061..b81ec59 100644
--- a/lib/nodelist.js
+++ b/lib/nodelist.js
@@ -1,97 +1,107 @@
 define(["sorttable", "virtual-dom", "numeral"], function (SortTable, V, numeral) {
   function getUptime(now, d) {
-    if (d.flags.online && "uptime" in d.statistics)
-      return Math.round(d.statistics.uptime)
-    else if (!d.flags.online && "lastseen" in d)
-      return Math.round(-(now.unix() - d.lastseen.unix()))
+    if (d.flags.online && "uptime" in d.statistics) {
+      return Math.round(d.statistics.uptime);
+    } else if (!d.flags.online && "lastseen" in d) {
+      return Math.round(-(now.unix() - d.lastseen.unix()));
+    }
   }
 
   function showUptime(uptime) {
-    var s = ""
-    uptime /= 3600
+    var s = "";
+    uptime /= 3600;
 
-    if (uptime !== undefined)
-      if (Math.abs(uptime) >= 24)
-        s = Math.round(uptime / 24) + "d"
-      else
-        s = Math.round(uptime) + "h"
+    if (uptime !== undefined) {
+      if (Math.abs(uptime) >= 24) {
+        s = Math.round(uptime / 24) + "d";
+      } else {
+        s = Math.round(uptime) + "h";
+      }
+    }
 
-    return s
+    return s;
   }
 
-  var headings = [{ name: "Knoten",
-                    sort: function (a, b) {
-                      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
-                  },
-                  { name: "Uptime",
-                    sort: function (a, b) {
-                      return a.uptime - b.uptime
-                    },
-                    reverse: true
-                  },
-                  { name: "#Links",
-                    sort: function (a, b) {
-                      return a.meshlinks - b.meshlinks
-                    },
-                    reverse: true
-                  },
-                  { name: "Clients",
-                    sort: function (a, b) {
-                      return ("clients" in a.statistics ? a.statistics.clients : -1) -
-                             ("clients" in b.statistics ? b.statistics.clients : -1)
-                    },
-                    reverse: true
-                  }]
+  var headings = [{
+    name: "Knoten",
+    sort: function (a, b) {
+      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
+  },
+    {
+      name: "Uptime",
+      sort: function (a, b) {
+        return a.uptime - b.uptime;
+      },
+      reverse: true
+    },
+    {
+      name: "#Links",
+      sort: function (a, b) {
+        return a.meshlinks - b.meshlinks;
+      },
+      reverse: true
+    },
+    {
+      name: "Clients",
+      sort: function (a, b) {
+        return ("clients" in a.statistics ? a.statistics.clients : -1) -
+          ("clients" in b.statistics ? b.statistics.clients : -1);
+      },
+      reverse: true
+    }];
 
-  return function(router) {
+  return function (router) {
     function renderRow(d) {
-      var td1Content = []
-      var aClass = ["hostname", d.flags.online ? "online" : "offline"]
+      var td1Content = [];
+      var aClass = ["hostname", d.flags.online ? "online" : "offline"];
 
-      td1Content.push(V.h("a", { className: aClass.join(" "),
-                                 onclick: router.node(d),
-                                 href: "#!n:" + d.nodeinfo.node_id
-                               }, d.nodeinfo.hostname))
+      td1Content.push(V.h("a", {
+        className: aClass.join(" "),
+        onclick: router.node(d),
+        href: "#!n:" + d.nodeinfo.node_id
+      }, d.nodeinfo.hostname));
 
-      if (has_location(d))
-        td1Content.push(V.h("span", {className: "icon ion-location"}))
+      if (has_location(d)) {
+        td1Content.push(V.h("span", {className: "icon ion-location"}));
+      }
 
-      var td1 = V.h("td", td1Content)
-      var td2 = V.h("td", showUptime(d.uptime))
-      var td3 = V.h("td", d.meshlinks.toString())
-      var td4 = V.h("td", numeral("clients" in d.statistics ? d.statistics.clients : "").format("0,0"))
+      var td1 = V.h("td", td1Content);
+      var td2 = V.h("td", showUptime(d.uptime));
+      var td3 = V.h("td", d.meshlinks.toString());
+      var td4 = V.h("td", numeral("clients" in d.statistics ? d.statistics.clients : "").format("0,0"));
 
-      return V.h("tr", [td1, td2, td3, td4])
+      return V.h("tr", [td1, td2, td3, td4]);
     }
 
-    var table = new SortTable(headings, 0, renderRow)
+    var table = new SortTable(headings, 0, renderRow);
 
     this.render = function (d) {
-      var el = document.createElement("div")
-      d.appendChild(el)
+      var el = document.createElement("div");
+      d.appendChild(el);
 
-      var h2 = document.createElement("h2")
-      h2.textContent = "Alle Knoten"
-      el.appendChild(h2)
+      var h2 = document.createElement("h2");
+      h2.textContent = "Alle Knoten";
+      el.appendChild(h2);
 
-      el.appendChild(table.el)
-    }
+      el.appendChild(table.el);
+    };
 
     this.setData = function (d) {
       var data = d.nodes.all.map(function (e) {
-        var n = Object.create(e)
-        n.uptime = getUptime(d.now, e) || 0
-        n.meshlinks = e.meshlinks || 0
-        return n
-      })
+        var n = Object.create(e);
+        n.uptime = getUptime(d.now, e) || 0;
+        n.meshlinks = e.meshlinks || 0;
+        return n;
+      });
 
-      table.setData(data)
-    }
-  }
-})
+      table.setData(data);
+    };
+  };
+});
diff --git a/lib/proportions.js b/lib/proportions.js
index f5b0b26..8523165 100644
--- a/lib/proportions.js
+++ b/lib/proportions.js
@@ -1,236 +1,278 @@
-define(["chroma-js", "virtual-dom", "numeral-intl", "filters/genericnode", "vercomp" ],
+define(["chroma-js", "virtual-dom", "numeral-intl", "filters/genericnode", "vercomp"],
   function (Chroma, V, numeral, Filter, vercomp) {
 
-  return function (config, filterManager) {
-    var self = this
-    var scale = Chroma.scale("YlGnBu").mode("lab")
+    return function (config, filterManager) {
+      var self = this;
+      var scale = Chroma.scale("YlGnBu").mode("lab");
 
-    var statusTable = document.createElement("table")
-    statusTable.classList.add("proportion")
+      var statusTable = document.createElement("table");
+      statusTable.classList.add("proportion");
 
-    var fwTable = document.createElement("table")
-    fwTable.classList.add("proportion")
+      var fwTable = document.createElement("table");
+      fwTable.classList.add("proportion");
 
-    var hwTable = document.createElement("table")
-    hwTable.classList.add("proportion")
+      var hwTable = document.createElement("table");
+      hwTable.classList.add("proportion");
 
-    var geoTable = document.createElement("table")
-    geoTable.classList.add("proportion")
+      var geoTable = document.createElement("table");
+      geoTable.classList.add("proportion");
 
-    var autoTable = document.createElement("table")
-    autoTable.classList.add("proportion")
+      var autoTable = document.createElement("table");
+      autoTable.classList.add("proportion");
 
-    var uplinkTable = document.createElement("table")
-    uplinkTable.classList.add("proportion")
+      var uplinkTable = document.createElement("table");
+      uplinkTable.classList.add("proportion");
 
-    var  gwNodesTable = document.createElement("table")
-    gwNodesTable.classList.add("proportion")
+      var gwNodesTable = document.createElement("table");
+      gwNodesTable.classList.add("proportion");
 
-    var gwClientsTable = document.createElement("table")
-    gwClientsTable.classList.add("proportion")
+      var gwClientsTable = document.createElement("table");
+      gwClientsTable.classList.add("proportion");
 
-    var siteTable = document.createElement("table")
-    siteTable.classList.add("proportion")
+      var siteTable = document.createElement("table");
+      siteTable.classList.add("proportion");
 
-    function showStatGlobal(o) {
-      return showStat(o)
-    }
-
-    function count(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] = 1 + (v in dict ? dict[v] : 0)
-      })
-
-      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) {
-      return function () {
-        filterManager.addFilter(filter)
-
-        return false
+      function showStatGlobal(o) {
+        return showStat(o);
       }
-    }
 
-    function fillTable(name, table, data) {
-      if (!table.last)
-        table.last = V.h("table")
+      function count(nodes, key, f) {
+        var dict = {};
 
-      var max = 0
-      data.forEach(function (d) {
-        if (d[1] > max)
-          max = d[1]
-      })
+        nodes.forEach(function (d) {
+          var v = dictGet(d, key.slice(0));
 
-      var items = data.map(function (d) {
-        var v = d[1] / max
-        var c1 = Chroma.contrast(scale(v), "white")
-        var c2 = Chroma.contrast(scale(v), "black")
+          if (f !== undefined) {
+            v = f(v);
+          }
 
-        var filter = new Filter(name, d[2], d[0], d[3])
+          if (v === null) {
+            return;
+          }
 
-        var a = V.h("a", { href: "#", onclick: addFilter(filter) }, d[0])
+          dict[v] = 1 + (v in dict ? dict[v] : 0);
+        });
 
-        var th = V.h("th", a)
-        var td = V.h("td", V.h("span", {style: {
-                                         width: Math.round(v * 100) + "%",
-                                         backgroundColor: scale(v).hex(),
-                                         color: c1 > c2 ? "white" : "black"
-                                       }}, numeral(d[1]).format("0,0")))
+        return Object.keys(dict).map(function (d) {
+          return [d, dict[d], key, f];
+        });
+      }
 
-        return V.h("tr", [th, td])
-      })
+      function countClients(nodes, key, f) {
+        var dict = {};
 
-      var tableNew = V.h("table", items)
-      table = V.patch(table, V.diff(table.last, tableNew))
-      table.last = tableNew
-    }
+        nodes.forEach(function (d) {
+          var v = dictGet(d, key.slice(0));
 
-    self.setData = function (data) {
-      var onlineNodes = data.nodes.all.filter(online)
-      var nodes = onlineNodes.concat(data.nodes.lost)
-      var nodeDict = {}
+          if (f !== undefined) {
+            v = f(v);
+          }
 
-      data.nodes.all.forEach(function (d) {
-        nodeDict[d.nodeinfo.node_id] = d
-      })
+          if (v === null) {
+            return;
+          }
 
-      var statusDict = count(nodes, ["flags", "online"], function (d) {
-        return d ? "online" : "offline"
-      })
-      var fwDict = count(nodes, ["nodeinfo", "software", "firmware", "release"])
-      var hwDict = count(nodes, ["nodeinfo", "hardware", "model"], function (d) {
-        if (d) {
-          d = d.replace(/\(r\)|\(tm\)/gi, "").replace(/AMD |Intel |TP-Link | CPU| Processor/g, "")
-          if (d.indexOf("@") > 0) d = d.substring(0, d.indexOf("@"))
+          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) {
+        return function () {
+          filterManager.addFilter(filter);
+
+          return false;
+        };
+      }
+
+      function fillTable(name, table, data) {
+        if (!table.last) {
+          table.last = V.h("table");
         }
-        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) {
-        if (d === null)
-          return null
-        else if (d.enabled)
-          return d.branch
-        else
-          return "(deaktiviert)"
-      })
+        var max = 0;
+        data.forEach(function (d) {
+          if (d[1] > max) {
+            max = d[1];
+          }
+        });
 
-      var uplinkDict = count(nodes, ["flags", "uplink"], function (d) {
-        return d ? "ja" : "nein"
-      })
+        var items = data.map(function (d) {
+          var v = d[1] / max;
+          var c1 = Chroma.contrast(scale(v), "white");
+          var c2 = Chroma.contrast(scale(v), "black");
 
-      var gwNodesDict = count(onlineNodes, ["statistics", "gateway"], function (d) {
-        if (d === null)
-          return null
+          var filter = new Filter(name, d[2], d[0], d[3]);
 
-        if (d.node)
-          return d.node.nodeinfo.hostname
+          var a = V.h("a", {href: "#", onclick: addFilter(filter)}, d[0]);
 
-        if (d.id)
-          return d.id
+          var th = V.h("th", a);
+          var td = V.h("td", V.h("span", {
+            style: {
+              width: Math.round(v * 100) + "%",
+              backgroundColor: scale(v).hex(),
+              color: c1 > c2 ? "white" : "black"
+            }
+          }, numeral(d[1]).format("0,0")));
 
-        return d
-      })
+          return V.h("tr", [th, td]);
+        });
 
-      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
-        if (config.siteNames)
-          config.siteNames.forEach( function (t) {
-            if(d === t.site)
-              rt = t.name
-          })
-        return rt
-      })
-
-      fillTable("Status", statusTable, statusDict.sort(function (a, b) { return b[1] - a[1] }))
-      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("Koordinaten", geoTable, geoDict.sort(function (a, b) { return b[1] - a[1] }))
-      fillTable("Uplink", uplinkTable, uplinkDict.sort(function (a, b) { return b[1] - a[1] }))
-      fillTable("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] }))
-    }
-
-
-    self.render = function (el) {
-      var h2
-      self.renderSingle(el, "Status", statusTable)
-      self.renderSingle(el, "Nodes an Gateway", gwNodesTable)
-      self.renderSingle(el, "Clients an Gateway", gwClientsTable)
-      self.renderSingle(el, "Firmwareversionen", fwTable)
-      self.renderSingle(el, "Uplink", uplinkTable)
-      self.renderSingle(el, "Hardwaremodelle", hwTable)
-      self.renderSingle(el, "Auf der Karte sichtbar", geoTable)
-      self.renderSingle(el, "Autoupdater", autoTable)
-      self.renderSingle(el, "Site", siteTable)
-
-      if (config.globalInfos)
-        config.globalInfos.forEach(function (globalInfo) {
-          h2 = document.createElement("h2")
-          h2.textContent = globalInfo.name
-          el.appendChild(h2)
-          el.appendChild(showStatGlobal(globalInfo))
-        })
+        var tableNew = V.h("table", items);
+        table = V.patch(table, V.diff(table.last, tableNew));
+        table.last = tableNew;
       }
 
-    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
-  }
-})
+      self.setData = function (data) {
+        var onlineNodes = data.nodes.all.filter(online);
+        var nodes = onlineNodes.concat(data.nodes.lost);
+        var nodeDict = {};
+
+        data.nodes.all.forEach(function (d) {
+          nodeDict[d.nodeinfo.node_id] = d;
+        });
+
+        var statusDict = count(nodes, ["flags", "online"], function (d) {
+          return d ? "online" : "offline";
+        });
+        var fwDict = count(nodes, ["nodeinfo", "software", "firmware", "release"]);
+        var hwDict = count(nodes, ["nodeinfo", "hardware", "model"], function (d) {
+          if (d) {
+            d = d.replace(/\(r\)|\(tm\)/gi, "").replace(/AMD |Intel |TP-Link | CPU| Processor/g, "");
+            if (d.indexOf("@") > 0) {
+              d = d.substring(0, d.indexOf("@"));
+            }
+          }
+          return d;
+        });
+        var geoDict = count(nodes, ["nodeinfo", "location"], function (d) {
+          return d && d.longitude && d.latitude ? "ja" : "nein";
+        });
+
+        var autoDict = count(nodes, ["nodeinfo", "software", "autoupdater"], function (d) {
+          if (d === null) {
+            return null;
+          } else if (d.enabled) {
+            return d.branch;
+          } else {
+            return "(deaktiviert)";
+          }
+        });
+
+        var uplinkDict = count(nodes, ["flags", "uplink"], function (d) {
+          return d ? "ja" : "nein";
+        });
+
+        var gwNodesDict = count(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 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;
+          if (config.siteNames) {
+            config.siteNames.forEach(function (t) {
+              if (d === t.site) {
+                rt = t.name;
+              }
+            });
+          }
+          return rt;
+        });
+
+        fillTable("Status", statusTable, statusDict.sort(function (a, b) {
+          return b[1] - a[1];
+        }));
+        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("Koordinaten", geoTable, geoDict.sort(function (a, b) {
+          return b[1] - a[1];
+        }));
+        fillTable("Uplink", uplinkTable, uplinkDict.sort(function (a, b) {
+          return b[1] - a[1];
+        }));
+        fillTable("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];
+        }));
+      };
+
+
+      self.render = function (el) {
+        var h2;
+        self.renderSingle(el, "Status", statusTable);
+        self.renderSingle(el, "Nodes an Gateway", gwNodesTable);
+        self.renderSingle(el, "Clients an Gateway", gwClientsTable);
+        self.renderSingle(el, "Firmwareversionen", fwTable);
+        self.renderSingle(el, "Uplink", uplinkTable);
+        self.renderSingle(el, "Hardwaremodelle", hwTable);
+        self.renderSingle(el, "Auf der Karte sichtbar", geoTable);
+        self.renderSingle(el, "Autoupdater", autoTable);
+        self.renderSingle(el, "Site", siteTable);
+
+        if (config.globalInfos) {
+          config.globalInfos.forEach(function (globalInfo) {
+            h2 = document.createElement("h2");
+            h2.textContent = globalInfo.name;
+            el.appendChild(h2);
+            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;
+    };
+  });
diff --git a/lib/router.js b/lib/router.js
index fb56bb1..f719cc0 100644
--- a/lib/router.js
+++ b/lib/router.js
@@ -1,214 +1,230 @@
 define(function () {
   return function () {
-    var self = this
-    var objects = { nodes: {}, links: {} }
-    var targets = []
-    var views = {}
-    var currentView
-    var currentObject
-    var running = false
+    var self = this;
+    var objects = {nodes: {}, links: {}};
+    var targets = [];
+    var views = {};
+    var currentView;
+    var currentObject;
+    var running = false;
 
     function saveState() {
-      var e = []
+      var e = [];
 
-      if (currentView)
-        e.push("v:" + currentView)
-
-      if (currentObject) {
-        if ("node" in currentObject)
-          e.push("n:" + encodeURIComponent(currentObject.node.nodeinfo.node_id))
-
-        if ("link" in currentObject)
-          e.push("l:" + encodeURIComponent(currentObject.link.id))
+      if (currentView) {
+        e.push("v:" + currentView);
       }
 
-      var s = "#!" + e.join(";")
+      if (currentObject) {
+        if ("node" in currentObject) {
+          e.push("n:" + encodeURIComponent(currentObject.node.nodeinfo.node_id));
+        }
 
-      window.history.pushState(s, undefined, s)
+        if ("link" in currentObject) {
+          e.push("l:" + encodeURIComponent(currentObject.link.id));
+        }
+      }
+
+      var s = "#!" + e.join(";");
+
+      window.history.pushState(s, undefined, s);
     }
 
     function resetView(push) {
-      push = trueDefault(push)
+      push = trueDefault(push);
 
-      targets.forEach( function (t) {
-        t.resetView()
-      })
+      targets.forEach(function (t) {
+        t.resetView();
+      });
 
       if (push) {
-        currentObject = undefined
-        saveState()
+        currentObject = undefined;
+        saveState();
       }
     }
 
     function gotoNode(d) {
-      if (!d)
-        return false
+      if (!d) {
+        return false;
+      }
 
-      targets.forEach( function (t) {
-        t.gotoNode(d)
-      })
+      targets.forEach(function (t) {
+        t.gotoNode(d);
+      });
 
-      return true
+      return true;
     }
 
     function gotoLink(d) {
-      if (!d)
-        return false
+      if (!d) {
+        return false;
+      }
 
-      targets.forEach( function (t) {
-        t.gotoLink(d)
-      })
+      targets.forEach(function (t) {
+        t.gotoLink(d);
+      });
 
-      return true
+      return true;
     }
 
     function gotoLocation(d) {
-      if (!d)
-        return false
+      if (!d) {
+        return false;
+      }
 
-      targets.forEach( function (t) {
-        if(!t.gotoLocation)console.warn("has no gotoLocation", t)
-        t.gotoLocation(d)
-      })
+      targets.forEach(function (t) {
+        if (!t.gotoLocation) {
+          console.warn("has no gotoLocation", t);
+        }
+        t.gotoLocation(d);
+      });
 
-      return true
+      return true;
     }
 
     function loadState(s) {
-      if (!s)
-        return false
+      if (!s) {
+        return false;
+      }
 
-      s = decodeURIComponent(s)
+      s = decodeURIComponent(s);
 
-      if (!s.startsWith("#!"))
-        return false
+      if (!s.startsWith("#!")) {
+        return false;
+      }
 
-      var targetSet = false
+      var targetSet = false;
 
       s.slice(2).split(";").forEach(function (d) {
-        var args = d.split(":")
+        var args = d.split(":");
 
         if (args[0] === "v" && args[1] in views) {
-          currentView = args[1]
-          views[args[1]]()
+          currentView = args[1];
+          views[args[1]]();
         }
 
-        var id
+        var id;
 
         if (args[0] === "n") {
-          id = args[1]
+          id = args[1];
           if (id in objects.nodes) {
-            currentObject = { node: objects.nodes[id] }
-            gotoNode(objects.nodes[id])
-            targetSet = true
+            currentObject = {node: objects.nodes[id]};
+            gotoNode(objects.nodes[id]);
+            targetSet = true;
           }
         }
 
         if (args[0] === "l") {
-          id = args[1]
+          id = args[1];
           if (id in objects.links) {
-            currentObject = { link: objects.links[id] }
-            gotoLink(objects.links[id])
-            targetSet = true
+            currentObject = {link: objects.links[id]};
+            gotoLink(objects.links[id]);
+            targetSet = true;
           }
         }
-      })
+      });
 
-      return targetSet
+      return targetSet;
     }
 
     self.start = function () {
-      running = true
+      running = true;
 
-      if (!loadState(window.location.hash))
-        resetView(false)
+      if (!loadState(window.location.hash)) {
+        resetView(false);
+      }
 
       window.onpopstate = function (d) {
-        if (!loadState(d.state))
-          resetView(false)
-      }
-    }
+        if (!loadState(d.state)) {
+          resetView(false);
+        }
+      };
+    };
 
     self.view = function (d) {
       if (d in views) {
-        views[d]()
+        views[d]();
 
-        if (!currentView || running)
-          currentView = d
-
-        if (!running)
-          return
-
-        saveState()
-
-        if (!currentObject) {
-          resetView(false)
-          return
+        if (!currentView || running) {
+          currentView = d;
         }
 
-        if ("node" in currentObject)
-          gotoNode(currentObject.node)
+        if (!running) {
+          return;
+        }
 
-        if ("link" in currentObject)
-          gotoLink(currentObject.link)
+        saveState();
+
+        if (!currentObject) {
+          resetView(false);
+          return;
+        }
+
+        if ("node" in currentObject) {
+          gotoNode(currentObject.node);
+        }
+
+        if ("link" in currentObject) {
+          gotoLink(currentObject.link);
+        }
       }
-    }
+    };
 
     self.node = function (d) {
       return function () {
         if (gotoNode(d)) {
-          currentObject = { node: d }
-          saveState()
+          currentObject = {node: d};
+          saveState();
         }
 
-        return false
-      }
-    }
+        return false;
+      };
+    };
 
     self.link = function (d) {
       return function () {
         if (gotoLink(d)) {
-          currentObject = { link: d }
-          saveState()
+          currentObject = {link: d};
+          saveState();
         }
 
-        return false
-      }
-    }
+        return false;
+      };
+    };
 
-    self.gotoLocation = gotoLocation
+    self.gotoLocation = gotoLocation;
 
     self.reset = function () {
-      resetView()
-    }
+      resetView();
+    };
 
     self.addTarget = function (d) {
-      targets.push(d)
-    }
+      targets.push(d);
+    };
 
     self.removeTarget = function (d) {
-      targets = targets.filter( function (e) {
-        return d !== e
-      })
-    }
+      targets = targets.filter(function (e) {
+        return d !== e;
+      });
+    };
 
     self.addView = function (k, d) {
-      views[k] = d
-    }
+      views[k] = d;
+    };
 
     self.setData = function (data) {
-      objects.nodes = {}
-      objects.links = {}
+      objects.nodes = {};
+      objects.links = {};
 
-      data.nodes.all.forEach( function (d) {
-        objects.nodes[d.nodeinfo.node_id] = d
-      })
+      data.nodes.all.forEach(function (d) {
+        objects.nodes[d.nodeinfo.node_id] = d;
+      });
 
-      data.graph.links.forEach( function (d) {
-        objects.links[d.id] = d
-      })
-    }
+      data.graph.links.forEach(function (d) {
+        objects.links[d.id] = d;
+      });
+    };
 
-    return self
-  }
-})
+    return self;
+  };
+});
diff --git a/lib/sidebar.js b/lib/sidebar.js
index 464c0fa..4c839cc 100644
--- a/lib/sidebar.js
+++ b/lib/sidebar.js
@@ -1,49 +1,50 @@
 define([], function () {
   return function (el) {
-    var self = this
+    var self = this;
 
-    var sidebar = document.createElement("div")
-    sidebar.classList.add("sidebar")
-    el.appendChild(sidebar)
+    var sidebar = document.createElement("div");
+    sidebar.classList.add("sidebar");
+    el.appendChild(sidebar);
 
-    var button = document.createElement("button")
-    sidebar.appendChild(button)
+    var button = document.createElement("button");
+    sidebar.appendChild(button);
 
-    button.classList.add("sidebarhandle")
+    button.classList.add("sidebarhandle");
     button.onclick = function () {
-      sidebar.classList.toggle("hidden")
-    }
+      sidebar.classList.toggle("hidden");
+    };
 
-    var container = document.createElement("div")
-    container.classList.add("container")
-    sidebar.appendChild(container)
+    var container = document.createElement("div");
+    container.classList.add("container");
+    sidebar.appendChild(container);
 
     self.getWidth = function () {
-      if (sidebar.classList.contains("hidden"))
-          return 0
+      if (sidebar.classList.contains("hidden")) {
+        return 0;
+      }
 
-      var small = window.matchMedia("(max-width: 630pt)")
-      return small.matches ? 0 : sidebar.offsetWidth
-    }
+      var small = window.matchMedia("(max-width: 630pt)");
+      return small.matches ? 0 : sidebar.offsetWidth;
+    };
 
     self.add = function (d) {
-      d.render(container)
-    }
+      d.render(container);
+    };
 
     self.ensureVisible = function () {
-      sidebar.classList.remove("hidden")
-    }
+      sidebar.classList.remove("hidden");
+    };
 
     self.hide = function () {
-      container.classList.add("hidden")
-    }
+      container.classList.add("hidden");
+    };
 
     self.reveal = function () {
-      container.classList.remove("hidden")
-    }
+      container.classList.remove("hidden");
+    };
 
-    self.container = sidebar
+    self.container = sidebar;
 
-    return self
-  }
-})
+    return self;
+  };
+});
diff --git a/lib/simplenodelist.js b/lib/simplenodelist.js
index 8a763df..a327203 100644
--- a/lib/simplenodelist.js
+++ b/lib/simplenodelist.js
@@ -1,63 +1,66 @@
 define(["moment", "virtual-dom"], function (moment, V) {
-  return function(nodes, field, router, title) {
-    var self = this
-    var el, tbody
+  return function (nodes, field, router, title) {
+    var self = this;
+    var el, tbody;
 
     self.render = function (d) {
-      el = document.createElement("div")
-      d.appendChild(el)
-    }
+      el = document.createElement("div");
+      d.appendChild(el);
+    };
 
     self.setData = function (data) {
-      var list = data.nodes[nodes]
+      var list = data.nodes[nodes];
 
       if (list.length === 0) {
-        while (el.firstChild)
-              el.removeChild(el.firstChild)
+        while (el.firstChild) {
+          el.removeChild(el.firstChild);
+        }
 
-        tbody = null
+        tbody = null;
 
-        return
+        return;
       }
 
       if (!tbody) {
-        var h2 = document.createElement("h2")
-        h2.textContent = title
-        el.appendChild(h2)
+        var h2 = document.createElement("h2");
+        h2.textContent = title;
+        el.appendChild(h2);
 
-        var table = document.createElement("table")
-        el.appendChild(table)
+        var table = document.createElement("table");
+        el.appendChild(table);
 
-        tbody = document.createElement("tbody")
-        tbody.last = V.h("tbody")
-        table.appendChild(tbody)
+        tbody = document.createElement("tbody");
+        tbody.last = V.h("tbody");
+        table.appendChild(tbody);
       }
 
-      var items = list.map( function (d) {
-        var time = moment(d[field]).from(data.now)
-        var td1Content = []
+      var items = list.map(function (d) {
+        var time = moment(d[field]).from(data.now);
+        var td1Content = [];
 
-        var aClass = ["hostname", d.flags.online ? "online" : "offline"]
+        var aClass = ["hostname", d.flags.online ? "online" : "offline"];
 
-        td1Content.push(V.h("a", { className: aClass.join(" "),
-                                   onclick: router.node(d),
-                                   href: "#!n:" + d.nodeinfo.node_id
-                                 }, d.nodeinfo.hostname))
+        td1Content.push(V.h("a", {
+          className: aClass.join(" "),
+          onclick: router.node(d),
+          href: "#!n:" + d.nodeinfo.node_id
+        }, d.nodeinfo.hostname));
 
-        if (has_location(d))
-          td1Content.push(V.h("span", {className: "icon ion-location"}))
+        if (has_location(d)) {
+          td1Content.push(V.h("span", {className: "icon ion-location"}));
+        }
 
-        var td1 = V.h("td", td1Content)
-        var td2 = V.h("td", time)
+        var td1 = V.h("td", td1Content);
+        var td2 = V.h("td", time);
 
-        return V.h("tr", [td1, td2])
-      })
+        return V.h("tr", [td1, td2]);
+      });
 
-      var tbodyNew = V.h("tbody", items)
-      tbody = V.patch(tbody, V.diff(tbody.last, tbodyNew))
-      tbody.last = tbodyNew
-    }
+      var tbodyNew = V.h("tbody", items);
+      tbody = V.patch(tbody, V.diff(tbody.last, tbodyNew));
+      tbody.last = tbodyNew;
+    };
 
-    return self
-  }
-})
+    return self;
+  };
+});
diff --git a/lib/sorttable.js b/lib/sorttable.js
index d6a39a1..881efba 100644
--- a/lib/sorttable.js
+++ b/lib/sorttable.js
@@ -1,57 +1,62 @@
 define(["virtual-dom"], function (V) {
-  return function(headings, sortIndex, renderRow) {
-    var data
-    var sortReverse = false
-    var el = document.createElement("table")
-    var elLast = V.h("table")
+  return function (headings, sortIndex, renderRow) {
+    var data;
+    var sortReverse = false;
+    var el = document.createElement("table");
+    var elLast = V.h("table");
 
     function sortTable(i) {
-      sortReverse = i === sortIndex ? !sortReverse : false
-      sortIndex = i
+      sortReverse = i === sortIndex ? !sortReverse : false;
+      sortIndex = i;
 
-      updateView()
+      updateView();
     }
 
     function sortTableHandler(i) {
-      return function () { sortTable(i) }
+      return function () {
+        sortTable(i);
+      };
     }
 
     function updateView() {
-      var children = []
+      var children = [];
 
       if (data.length !== 0) {
         var th = headings.map(function (d, i) {
-          var properties = { onclick: sortTableHandler(i),
-                             className: "sort-header"
-                           }
+          var properties = {
+            onclick: sortTableHandler(i),
+            className: "sort-header"
+          };
 
-          if (sortIndex === i)
-            properties.className += sortReverse ? " sort-up" : " sort-down"
+          if (sortIndex === i) {
+            properties.className += sortReverse ? " sort-up" : " sort-down";
+          }
 
-          return V.h("th", properties, d.name)
-        })
+          return V.h("th", properties, d.name);
+        });
 
-        var links = data.slice(0).sort(headings[sortIndex].sort)
+        var links = data.slice(0).sort(headings[sortIndex].sort);
 
-        if (headings[sortIndex].reverse ? !sortReverse : sortReverse)
-          links = links.reverse()
+        if (headings[sortIndex].reverse ? !sortReverse : sortReverse) {
+          links = links.reverse();
+        }
 
-        children.push(V.h("thead", V.h("tr", th)))
-        children.push(V.h("tbody", links.map(renderRow)))
+        children.push(V.h("thead", V.h("tr", th)));
+        children.push(V.h("tbody", links.map(renderRow)));
       }
 
-      var elNew = V.h("table", children)
-      el = V.patch(el, V.diff(elLast, elNew))
-      elLast = elNew
+      var elNew = V.h("table", children);
+      el = V.patch(el, V.diff(elLast, elNew));
+      elLast = elNew;
     }
 
     this.setData = function (d) {
-      data = d
-      updateView()
-    }
+      data = d;
+      updateView();
+    };
 
-    this.el = el
+    this.el = el;
 
-    return this
-  }
-})
+    return this;
+  };
+});
diff --git a/lib/tabs.js b/lib/tabs.js
index 7605742..f4fed6d 100644
--- a/lib/tabs.js
+++ b/lib/tabs.js
@@ -1,57 +1,61 @@
 define([], function () {
   return function () {
-    var self = this
+    var self = this;
 
-    var tabs = document.createElement("ul")
-    tabs.classList.add("tabs")
+    var tabs = document.createElement("ul");
+    tabs.classList.add("tabs");
 
-    var container = document.createElement("div")
+    var container = document.createElement("div");
 
     function gotoTab(li) {
-      for (var i = 0; i < tabs.children.length; i++)
-        tabs.children[i].classList.remove("visible")
+      for (var i = 0; i < tabs.children.length; i++) {
+        tabs.children[i].classList.remove("visible");
+      }
 
-      while (container.firstChild)
-        container.removeChild(container.firstChild)
+      while (container.firstChild) {
+        container.removeChild(container.firstChild);
+      }
 
-      li.classList.add("visible")
+      li.classList.add("visible");
 
-      var tab = document.createElement("div")
-      tab.classList.add("tab")
-      container.appendChild(tab)
-      li.child.render(tab)
+      var tab = document.createElement("div");
+      tab.classList.add("tab");
+      container.appendChild(tab);
+      li.child.render(tab);
     }
 
     function switchTab() {
-      gotoTab(this)
+      gotoTab(this);
 
-      return false
+      return false;
     }
 
     self.add = function (title, d) {
-      var li = document.createElement("li")
-      li.textContent = title
-      li.onclick = switchTab
-      li.child = d
-      tabs.appendChild(li)
+      var li = document.createElement("li");
+      li.textContent = title;
+      li.onclick = switchTab;
+      li.child = d;
+      tabs.appendChild(li);
 
-      var anyVisible = false
+      var anyVisible = false;
 
-      for (var i = 0; i < tabs.children.length; i++)
+      for (var i = 0; i < tabs.children.length; i++) {
         if (tabs.children[i].classList.contains("visible")) {
-          anyVisible = true
-          break
+          anyVisible = true;
+          break;
         }
+      }
 
-      if (!anyVisible)
-        gotoTab(li)
-    }
+      if (!anyVisible) {
+        gotoTab(li);
+      }
+    };
 
     self.render = function (el) {
-      el.appendChild(tabs)
-      el.appendChild(container)
-    }
+      el.appendChild(tabs);
+      el.appendChild(container);
+    };
 
-    return self
-  }
-})
+    return self;
+  };
+});
diff --git a/lib/title.js b/lib/title.js
index e9377a4..4c99c2c 100644
--- a/lib/title.js
+++ b/lib/title.js
@@ -1,35 +1,38 @@
 define(function () {
-   return function (config) {
+  return function (config) {
     function setTitle(d) {
-      var title = [config.siteName]
+      var title = [config.siteName];
 
-      if (d !== undefined)
-        title.push(d)
+      if (d !== undefined) {
+        title.push(d);
+      }
 
-      document.title = title.join(": ")
+      document.title = title.join(": ");
     }
 
     this.resetView = function () {
-      setTitle()
-    }
+      setTitle();
+    };
 
     this.gotoNode = function (d) {
-      if (d)
-        setTitle(d.nodeinfo.hostname)
-    }
+      if (d) {
+        setTitle(d.nodeinfo.hostname);
+      }
+    };
 
     this.gotoLink = function (d) {
-      if (d)
-        setTitle((d.source.node ? d.source.node.nodeinfo.hostname : d.source.id) + " – " + d.target.node.nodeinfo.hostname)
-    }
+      if (d) {
+        setTitle((d.source.node ? d.source.node.nodeinfo.hostname : d.source.id) + " – " + d.target.node.nodeinfo.hostname);
+      }
+    };
 
-    this.gotoLocation = function() {
+    this.gotoLocation = function () {
       //ignore
-    }
+    };
 
     this.destroy = function () {
-    }
+    };
 
-    return this
-  }
-})
+    return this;
+  };
+});
diff --git a/lib/vercomp.js b/lib/vercomp.js
index 428f258..4752b11 100644
--- a/lib/vercomp.js
+++ b/lib/vercomp.js
@@ -1,60 +1,68 @@
 define([], function () {
   function order(c) {
-    if (/^\d$/.test(c))
-      return 0
-    else if (/^[a-z]$/i.test(c))
-      return c.charCodeAt(0)
-    else if (c === "~")
-      return -1
-    else if (c)
-      return c.charCodeAt(0) + 256
-    else
-      return 0
+    if (/^\d$/.test(c)) {
+      return 0;
+    } else if (/^[a-z]$/i.test(c)) {
+      return c.charCodeAt(0);
+    } else if (c === "~") {
+      return -1;
+    } else if (c) {
+      return c.charCodeAt(0) + 256;
+    } else {
+      return 0;
+    }
   }
 
   // Based on dpkg code
   function vercomp(a, b) {
-    var apos = 0, bpos = 0
+    var apos = 0, bpos = 0;
     while (apos < a.length || bpos < b.length) {
-      var firstDiff = 0
+      var firstDiff = 0;
 
       while ((apos < a.length && !/^\d$/.test(a[apos])) || (bpos < b.length && !/^\d$/.test(b[bpos]))) {
-        var ac = order(a[apos])
-        var bc = order(b[bpos])
+        var ac = order(a[apos]);
+        var bc = order(b[bpos]);
 
-        if (ac !== bc)
-          return ac - bc
+        if (ac !== bc) {
+          return ac - bc;
+        }
 
-        apos++
-        bpos++
+        apos++;
+        bpos++;
       }
 
-      while (a[apos] === "0")
-        apos++
+      while (a[apos] === "0") {
+        apos++;
+      }
 
-      while (b[bpos] === "0")
-        bpos++
+      while (b[bpos] === "0") {
+        bpos++;
+      }
 
       while (/^\d$/.test(a[apos]) && /^\d$/.test(b[bpos])) {
-        if (firstDiff === 0)
-          firstDiff = a.charCodeAt(apos) - b.charCodeAt(bpos)
+        if (firstDiff === 0) {
+          firstDiff = a.charCodeAt(apos) - b.charCodeAt(bpos);
+        }
 
-        apos++
-        bpos++
+        apos++;
+        bpos++;
       }
 
-      if (/^\d$/.test(a[apos]))
-        return 1
+      if (/^\d$/.test(a[apos])) {
+        return 1;
+      }
 
-      if (/^\d$/.test(b[bpos]))
-        return -1
+      if (/^\d$/.test(b[bpos])) {
+        return -1;
+      }
 
-      if (firstDiff !== 0)
-        return firstDiff
+      if (firstDiff !== 0) {
+        return firstDiff;
+      }
     }
 
-    return 0
+    return 0;
   }
 
-  return vercomp
-})
+  return vercomp;
+});
diff --git a/scss/main.scss b/scss/main.scss
index 47cbed9..0415212 100644
--- a/scss/main.scss
+++ b/scss/main.scss
@@ -131,14 +131,14 @@ table.attributes tr.routerpic td {
   font-weight: bold;
   /*background-color: red;*/
   font-size: large;
-  vertical-align:bottom;
+  vertical-align: bottom;
 }
 
 table.attributes tr.routerpic th {
   font-weight: bold;
   /*background-color: red;*/
   font-size: large;
-  vertical-align:bottom;
+  vertical-align: bottom;
 }
 
 .nodenamesidebar {
@@ -146,7 +146,7 @@ table.attributes tr.routerpic th {
   font-weight: bold;
   /*background-color: red;*/
   font-size: large;
-  vertical-align:bottom;
+  vertical-align: bottom;
   bottom: 0px;
 }
 
diff --git a/tasks/build.js b/tasks/build.js
index d76ccfc..2fd1e83 100644
--- a/tasks/build.js
+++ b/tasks/build.js
@@ -1,11 +1,11 @@
-module.exports = function(grunt) {
+module.exports = function (grunt) {
   grunt.config.merge({
     bowerdir: "bower_components",
     copy: {
       html: {
         options: {
           process: function (content) {
-            return content.replace("#revision#", grunt.option("gitRevision"))
+            return content.replace("#revision#", grunt.option("gitRevision"));
           }
         },
         src: ["*.html"],
@@ -26,31 +26,31 @@ module.exports = function(grunt) {
         dest: "build/vendor/"
       },
       robotoSlab: {
-        src: [ "fonts/*",
-               "roboto-slab-fontface.css"
-             ],
+        src: ["fonts/*",
+          "roboto-slab-fontface.css"
+        ],
         expand: true,
         dest: "build/",
         cwd: "bower_components/roboto-slab-fontface"
       },
       roboto: {
-        src: [ "fonts/*",
-               "roboto-fontface.css"
-             ],
+        src: ["fonts/*",
+          "roboto-fontface.css"
+        ],
         expand: true,
         dest: "build/",
         cwd: "bower_components/roboto-fontface"
       },
       ionicons: {
-        src: [ "fonts/*",
-               "css/ionicons.min.css"
-             ],
+        src: ["fonts/*",
+          "css/ionicons.min.css"
+        ],
         expand: true,
         dest: "build/",
         cwd: "bower_components/ionicons/"
       },
       leafletImages: {
-        src: [ "images/*" ],
+        src: ["images/*"],
         expand: true,
         dest: "build/",
         cwd: "bower_components/leaflet/dist/"
@@ -83,26 +83,26 @@ module.exports = function(grunt) {
     cssmin: {
       target: {
         files: {
-          "build/style.css": [ "bower_components/leaflet/dist/leaflet.css",
-                               "bower_components/Leaflet.label/dist/leaflet.label.css",
-                               "style.css"
-                             ]
+          "build/style.css": ["bower_components/leaflet/dist/leaflet.css",
+            "bower_components/Leaflet.label/dist/leaflet.label.css",
+            "style.css"
+          ]
         }
       }
     },
     "bower-install-simple": {
-        options: {
-          directory: "<%=bowerdir%>",
-          color: true,
-          interactive: false,
-          production: true
-        },
-        "prod": {
-          options: {
-            production: true
-          }
-        }
+      options: {
+        directory: "<%=bowerdir%>",
+        color: true,
+        interactive: false,
+        production: true
       },
+      "prod": {
+        options: {
+          production: true
+        }
+      }
+    },
     requirejs: {
       compile: {
         options: {
@@ -116,11 +116,11 @@ module.exports = function(grunt) {
         }
       }
     }
-  })
+  });
 
-  grunt.loadNpmTasks("grunt-bower-install-simple")
-  grunt.loadNpmTasks("grunt-contrib-copy")
-  grunt.loadNpmTasks("grunt-contrib-requirejs")
-  grunt.loadNpmTasks("grunt-sass")
-  grunt.loadNpmTasks("grunt-postcss")
-}
+  grunt.loadNpmTasks("grunt-bower-install-simple");
+  grunt.loadNpmTasks("grunt-contrib-copy");
+  grunt.loadNpmTasks("grunt-contrib-requirejs");
+  grunt.loadNpmTasks("grunt-sass");
+  grunt.loadNpmTasks("grunt-postcss");
+};
diff --git a/tasks/clean.js b/tasks/clean.js
index 989ef08..ed0e234 100644
--- a/tasks/clean.js
+++ b/tasks/clean.js
@@ -3,7 +3,7 @@ module.exports = function (grunt) {
     clean: {
       build: ["build/**/*", "node_modules/grunt-newer/.cache"]
     }
-  })
+  });
 
-  grunt.loadNpmTasks("grunt-contrib-clean")
-}
+  grunt.loadNpmTasks("grunt-contrib-clean");
+};
diff --git a/tasks/development.js b/tasks/development.js
index 6c799d2..2c00cd7 100644
--- a/tasks/development.js
+++ b/tasks/development.js
@@ -24,8 +24,8 @@ module.exports = function (grunt) {
         tasks: []
       }
     }
-  })
+  });
 
-  grunt.loadNpmTasks("grunt-contrib-connect")
-  grunt.loadNpmTasks("grunt-contrib-watch")
-}
+  grunt.loadNpmTasks("grunt-contrib-connect");
+  grunt.loadNpmTasks("grunt-contrib-watch");
+};
diff --git a/tasks/linting.js b/tasks/linting.js
index a05bce0..71311dc 100644
--- a/tasks/linting.js
+++ b/tasks/linting.js
@@ -31,8 +31,8 @@ module.exports = function (grunt) {
         src: ["Gruntfile.js", "tasks/*.js"]
       }
     }
-  })
+  });
 
-  grunt.loadNpmTasks("grunt-check-dependencies")
-  grunt.loadNpmTasks("grunt-eslint")
-}
+  grunt.loadNpmTasks("grunt-check-dependencies");
+  grunt.loadNpmTasks("grunt-eslint");
+};

From 9afb214360a3adfd6e3331089cdbbc907ceffb32 Mon Sep 17 00:00:00 2001
From: Xaver Maierhofer <xaver.maierhofer@xwissen.info>
Date: Sun, 22 May 2016 15:26:16 +0200
Subject: [PATCH 03/32] [TASK] Use own iconfont set Reduce overhead in initial
 load

---
 assets/icons/fonts/hopglass.eot   | Bin 0 -> 3352 bytes
 assets/icons/fonts/hopglass.svg   |  27 +++++++++
 assets/icons/fonts/hopglass.ttf   | Bin 0 -> 3184 bytes
 assets/icons/fonts/hopglass.woff  | Bin 0 -> 2300 bytes
 assets/icons/fonts/hopglass.woff2 | Bin 0 -> 1700 bytes
 assets/icons/hopglass-icons.css   |  92 ++++++++++++++++++++++++++++++
 bower.json                        |   1 -
 html/index.html                   |   2 +-
 index.html                        |   2 +-
 lib/map.js                        |   2 +-
 tasks/build.js                    |   4 +-
 11 files changed, 124 insertions(+), 6 deletions(-)
 create mode 100644 assets/icons/fonts/hopglass.eot
 create mode 100644 assets/icons/fonts/hopglass.svg
 create mode 100644 assets/icons/fonts/hopglass.ttf
 create mode 100644 assets/icons/fonts/hopglass.woff
 create mode 100644 assets/icons/fonts/hopglass.woff2
 create mode 100644 assets/icons/hopglass-icons.css

diff --git a/assets/icons/fonts/hopglass.eot b/assets/icons/fonts/hopglass.eot
new file mode 100644
index 0000000000000000000000000000000000000000..c98534acce803009679a9b96251e6a6796287354
GIT binary patch
literal 3352
zcmd^C-EULb5udr&_tSPhZO4VgzSmCTB#<wEE(Ayj#n6a~EJcK_P-~NzB+7>^ClF8x
zic}szydYFo`#`IR)v8jrTJf;0x?8p4t^0sfX)Cr5Z6&&^R@&}EA!MaKtU`b1+J=<f
zZU2BC+xO1QnVCCh=6B8=2faik4-u0@Vo0PI4gtwc7Syrv3QrByJ7y|FwpXv!WH0Uw
zG($xy(KO9do|f<-s-qKBfaJU*D?<{XQ;?J~I}7bavS^U{XaF-`We>4<sF&7Artxg{
z<hPfU1;ph!32Ez*ZywIlK~ljd!CS|_(;pwXl6w<Ir@^0^T*#NMZ~Wp@@Q?6&e>%TZ
zdiD&S$g>;s`t<yza}&qEc#lZ_fJph(Od&s|{oTEaxj*BVo`FFAr=-Bv2flq~Vfmu5
zONK4q13xfdoXmUQac{x?4)~sh{KXP|mlBwN1-!MGUnso&#allhGS(owRVpqm54b<f
z6S?7!{m$C3zH6(t{QdfUpOd>%i$6^MZ3On@+%2;Bk%*u4YN=p~uaNXr^g)X(Z67X2
z;zMGwS`4Acj%-A*7%UOrB!zT7#^=Bvsop=QZ_w54co53ZApm;(+~Fx2dE(l<!jGOk
zqCV#;ygQ>3QRRW4bEw=8=_XnD<f<Qw=0KI`y2B%RFciPms*iWU32&|BqBn4lz}6*J
z@Wl1R`=sMZV&lxl?>7Fp@#)6q(_2s9eEQL5>*lG=E2u1nqtpH+orf=U>h-#<x<_>n
zy>EN>c{AR2Z^*Oine-&xo9<t`Z`al#e0}|3_4$HDA}h<?t}f#L{~10?(`nS|W48Dx
zm-#0Doc|%Yqyy3!>8f-~61#~-wRxS_$xYM|^+!|DR3eq|CwTov&kcM(6PFv%Tcaqi
z@?Qk#nltHs?yP6k7v{h)r(+-eRMSG5@j#Xz7$7vJcQhlUYY$~peyC|-%{jyvn2YjS
z#RtbaVF`O5{J>j=dxR516HZ|KHBN90V&|2xx5R#r#5(&qmCPg)=~S|F7e`dTuO;pe
zw6UgYY82cMw<I!&IHviSB)R1#Q}XEM8B_O2W|Qoer1tt(J-X*rL*-UU^<Gd+nKiRc
z)9Or(6<xXDRi);3|Gj&Lr$Zn|7CQzzf1T>drd~)Q^&FNm>3EojrI>~w)NKL{vd*Yb
zVZQvY*G$tgjqLkb19$UVLnn-5ueeRyFuSI@4Z}9yFjJ<x&2F<i?i8<|G!1Bvj6m5i
zkEP`VtPMRH)ggY}NSUV3XPT*MOlyc~4KcM)Oyot?LUJO0K+5#78tF`?;{jhRnTbYJ
zUm%{|&uTQ%$+Wj{FcR6fuRS_C9Bxa@wT-1xW2x74JrD}*4w+_M6R!;)jD~kN1qNE%
z_8k3FdMuqDJM1>YyTjptrZtIt2>bv&{5?FG1T5@$GHPeiu}%$n06?Tb{I`m7h}(31
z>ytw+m#Q9TF~(i4yNWi}Fs5QmumoI6r*Zgg_tyc4*pXzyj&)|zEdd8Ck{aPrINbU~
z*SYO~JKPpyPGGkKSVKW1U?XA20gE3?*Ho`*T$AKR4mG|f-7$<ivaB>TxVEn^hul&#
zB8U%508`YC_<fVG+@T7?t&bb|h+`=!F2_>f#B$yc8ihqosFM3+OCQHlQGXq4QGX)D
zE&fF60A~`ZTAA1$?e8D$FKW3%J%@6dZP$pqmnSAJPw+S<`$s+hG3Ds|46_rLD>2W}
z9N*^KVo!5Si)k%dOQt0g<J+N?mD-n|Qy_rr9Nro0{2}V-#LH3asEFFe08JbF0z)i-
zlkBX;nZZ0@<>!mOp%X*ChG4MaF)kOaV9*K%F1IJ!Ios7eG}PS{Z1_OT;_^#O1+AZS
z?Pzb`fk0)vZANe^-N*aR6t&4TMjI0;8J9*4$B;j9(|Ww~t;g1Q!$^(?w(eP++q%a}
z$M^`|JJ(^m0^2$YI@$I60FVQ&R1y(ZLi6$9+3Zj#q%?GgcjWqPyU*tP*?8h=cb6I%
z-al@|=k1oDZ3h8efxFMJem|n3X4`M)cx3B7`+MN~#TnTT{Gfg~bkf5d&xk`O7EPzp
zNK}}`*%RV~gHs$osIcm{lRT$ea=pT;tZ9-eNvv>Jwu==-Wgy-q8<MQl%a)$vksrwl
zOIo+7hdn#lrLebcXLt9`9yWLfw1aw6hf9OK?6WLH<)PORnHY>7q^U|vq|vS_EmJ$K
zR%sV>-mTIKaPhE8tB^m%n%QMQA{vScEg`QvtF%l<(LGh<F6i8?(hAzf-&AQ8^1pt$
z)to7orswlZXgXi&I8PL&&(G)4dS;z|bJ@9<(UTUiapF$zY0+NLFL$pM=^*XF;hVxQ
zj%I2QFFMK=7nife@^rz9_YGKkt=gkO4({nq^~MJVq4lyms;Qwd{pt?*RH3{yTU@jT
z`v(52IXT^&QxNA;sm%+XNOY@c>{()&UBH3L8tbU8$!_+rm+QEm8@Q31-1)_+a&dO5
zH$OGynVczHC>Ixd=L_eSy)~vhJ3X^3mu44TE3@Zj<-(;x^K5acH(xFnS9+IcW-IEx
zmt>BWSy4J)>OJeJkW<B##on`ZwTFuSdAZ=Nv6J(~rGlr%VRB9vZYPVSOS+?q<UF@6
z7Z!>a3Z_sfA#cUS%K8~ypg7ybUk`HjtH>4uJI%O?gI08FB0Crl_TT-zxNq;b{|y4D
B599y<

literal 0
HcmV?d00001

diff --git a/assets/icons/fonts/hopglass.svg b/assets/icons/fonts/hopglass.svg
new file mode 100644
index 0000000..da13e93
--- /dev/null
+++ b/assets/icons/fonts/hopglass.svg
@@ -0,0 +1,27 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg">
+<metadata>Generated by Fontastic.me</metadata>
+<defs>
+<font id="hopglass" horiz-adv-x="512">
+<font-face font-family="hopglass" units-per-em="512" ascent="480" descent="-32"/>
+<missing-glyph horiz-adv-x="512" />
+
+<glyph glyph-name="android-add" unicode="&#62151;" d="M416 235l-139 0 0-139-42 0 0 139-139 0 0 42 139 0 0 139 42 0 0-139 139 0z"/>
+<glyph glyph-name="chevron-left" unicode="&#61732;" d="M214 256l167 174c4 4 4 11 0 16l-30 30c-4 5-11 5-16 1l-204-213c-2-2-3-5-3-8 0-3 1-6 3-8l204-213c5-4 12-4 16 0l30 31c4 4 4 11 0 16z"/>
+<glyph glyph-name="chevron-right" unicode="&#61733;" d="M298 256l-167 174c-4 4-4 11 0 16l30 30c5 5 12 5 16 1l204-213c2-2 3-5 3-8 0-3-1-6-3-8l-204-213c-4-4-11-4-16 0l-30 31c-4 4-4 11 0 16z"/>
+<glyph glyph-name="pin" unicode="&#62371;" d="M332 284c-1 0-3 1-4 2-7 4-12 11-14 19l-18 118 0 5c0 7 4 11 10 13 0 1 1 1 2 1 7 4 12 8 12 17 0 20-7 21-18 21l-92 0c-11 0-18-1-18-21 0-9 5-13 12-17 1 0 2 0 2-1 6-3 10-6 10-13l0-5-18-118c-2-8-7-15-14-19-1-1-2-2-4-2-19-10-36-31-36-55 0-16 4-21 15-21l81 0 12-176 8 0 12 176 81 0c12 0 15 5 15 21 0 24-16 45-36 55z"/>
+<glyph glyph-name="wifi" unicode="&#62044;" d="M256 416c-80 0-156-30-214-84l-10-10 10-9 32-32 10-10 9 9c45 41 102 64 163 64 60 0 118-23 163-64l9-9 10 10 32 32 10 9-10 10c-58 54-134 84-214 84z m141-160c-38 35-88 54-141 54l-9 0 0 0c-49-2-96-21-132-54l-11-10 10-10 33-32 9-9 10 8c25 22 57 35 90 35 33 0 65-13 91-35l9-8 9 9 33 32 10 10z m-141-160l10 9 53 53 10 10-11 10c-16 11-33 20-62 20-29 0-45-10-61-20l-12-10 11-10 53-53z"/>
+<glyph glyph-name="eye" unicode="&#61747;" d="M256 384c-82 0-146-49-224-128 67-68 124-128 224-128 100 0 173 76 224 127-52 58-125 129-224 129z m0-219c-49 0-90 41-90 91 0 50 41 91 90 91 49 0 90-41 90-91 0-50-41-91-90-91z m0 123c0 8 3 15 8 21-3 0-5 0-8 0-29 0-52-24-52-53 0-29 23-53 52-53 29 0 52 24 52 53 0 2 0 5 0 7-6-4-12-7-20-7-18 0-32 14-32 32z"/>
+<glyph glyph-name="ios-arrow-thin-left" unicode="&#62421;" d="M190 162c3 3 3 8 0 12l-67 74 285 0c4 0 8 4 8 8 0 4-4 8-8 8l-285 0 67 74c3 4 3 8 0 12-3 3-9 3-12 0 0 0-79-87-80-88 0-1-2-3-2-6 0-3 2-5 2-6 1-1 80-88 80-88 2-1 4-2 6-2 2 0 4 1 6 2z"/>
+<glyph glyph-name="ios-arrow-thin-right" unicode="&#62422;" d="M322 162c-3 3-3 8 0 12l67 74-285 0c-4 0-8 4-8 8 0 4 4 8 8 8l285 0-67 74c-3 4-3 8 0 12 3 3 9 3 12 0 0 0 79-87 80-88 0-1 2-3 2-6 0-3-2-5-2-6-1-1-80-88-80-88-2-1-4-2-6-2-2 0-4 1-6 2z"/>
+<glyph glyph-name="arrow-up-b" unicode="&#61709;" d="M413 185l-2 2-136 156c-4 6-11 9-19 9-8 0-14-4-19-9l-136-156-2-3c-2-2-3-5-3-8 0-9 7-16 17-16l286 0c10 0 17 7 17 16 0 3-1 6-3 9z"/>
+<glyph glyph-name="arrow-down-b" unicode="&#61700;" d="M99 327l2-2 136-156c4-6 11-9 19-9 8 0 15 3 19 9l136 156 2 2c2 3 3 6 3 9 0 9-7 16-17 16l-286 0c-10 0-17-7-17-16 0-3 1-6 3-9z"/>
+<glyph glyph-name="android-locate" unicode="&#62185;" d="M256 336c-44 0-80-36-80-80 0-44 36-80 80-80 44 0 80 36 80 80 0 44-36 80-80 80z m191-59c-10 89-81 160-170 170l0 33-42 0 0-33c-89-10-160-81-170-170l-33 0 0-42 33 0c10-89 81-160 170-170l0-33 42 0 0 33c89 10 160 81 170 170l33 0 0 42z m-191-170c-82 0-149 67-149 149 0 82 67 149 149 149 82 0 149-67 149-149 0-82-67-149-149-149z"/>
+<glyph glyph-name="android-close" unicode="&#62167;" d="M405 375l-30 30-119-119-119 119-30-30 119-119-119-119 30-30 119 119 119-119 30 30-119 119z"/>
+<glyph glyph-name="android-lock" unicode="&#62354;" d="M376 326l-20 0 0 40c0 55-45 100-100 100-55 0-100-45-100-100l0-40-20 0c-22 0-40-18-40-40l0-200c0-22 18-40 40-40l240 0c22 0 40 18 40 40l0 200c0 22-18 40-40 40z m-120-182c-22 0-40 18-40 40 0 22 18 40 40 40 22 0 40-18 40-40 0-22-18-40-40-40z m62 182l-124 0 0 40c0 34 28 62 62 62 34 0 62-28 62-62z"/>
+<glyph glyph-name="ios-copy" unicode="&#62492;" d="M144 96l0 304-32 0 0-336 240 0 0 32z m181 352l-165 0 0-336 240 0 0 261z m43-112l-80 0 0 80 16 0 0-64 64 0z"/>
+<glyph glyph-name="location" unicode="&#62550;" d="M256 448c-66 0-119-54-119-120 0-115 119-264 119-264 0 0 119 149 119 264 0 66-53 120-119 120z m0-178c-31 0-56 25-56 56 0 32 25 57 56 57 31 0 56-25 56-57 0-31-25-56-56-56z"/>
+<glyph glyph-name="android-remove" unicode="&#62196;" d="M96 277l320 0 0-42-320 0z"/>
+<glyph glyph-name="ios-person" unicode="&#62590;" d="M363 148c-13 5-31 6-43 9-7 1-17 5-20 9-3 4-1 41-1 41 0 0 6 10 9 18 4 8 7 31 7 31 0 0 7 0 9 12 3 13 7 19 6 28 0 9-5 10-5 10 0 0 5 13 5 42 1 34-25 68-74 68-49 0-75-34-74-68 0-28 5-42 5-42 0 0-5-1-5-10-1-9 3-14 6-27 2-12 9-12 9-12 0 0 4-24 7-32 3-8 9-18 9-18 0 0 2-37-1-41-3-4-13-8-20-9-12-3-30-4-43-9-13-5-53-20-53-52l320 0c0 32-40 47-53 52z"/>
+</font></defs></svg>
diff --git a/assets/icons/fonts/hopglass.ttf b/assets/icons/fonts/hopglass.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..d117d0cd3e44fa916e7e29fbe10a9d730601881a
GIT binary patch
literal 3184
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

literal 0
HcmV?d00001

diff --git a/assets/icons/fonts/hopglass.woff b/assets/icons/fonts/hopglass.woff
new file mode 100644
index 0000000000000000000000000000000000000000..b1a185a83939c6c289fd9d803986ba16ec3af579
GIT binary patch
literal 2300
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

literal 0
HcmV?d00001

diff --git a/assets/icons/fonts/hopglass.woff2 b/assets/icons/fonts/hopglass.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..cce227441683b22d72d300daabbcad81e633863a
GIT binary patch
literal 1700
zcmV;V23z@ePew8T0RR9100yK04FCWD01S8l00vC}0RR9100000000000000000000
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

literal 0
HcmV?d00001

diff --git a/assets/icons/hopglass-icons.css b/assets/icons/hopglass-icons.css
new file mode 100644
index 0000000..f3ab178
--- /dev/null
+++ b/assets/icons/hopglass-icons.css
@@ -0,0 +1,92 @@
+@font-face {
+    font-family: "Ionicons";
+    src: url("fonts/hopglass.woff2") format("woff2"),
+    url("fonts/hopglass.woff") format("woff"),
+    url("fonts/hopglass.ttf") format("truetype");
+    font-weight: normal;
+    font-style: normal;
+
+}
+
+[class^="ion-"]:before,
+[class*=" ion-"]:before, .ion-inside {
+    display: inline-block;
+    font-family: "ionicons" !important;
+    speak: none;
+    font-style: normal;
+    font-weight: normal;
+    font-variant: normal;
+    text-transform: none;
+    text-rendering: auto;
+    line-height: 1;
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+}
+
+.ion-android-add:before {
+    content: "\f2c7";
+}
+
+.ion-chevron-left:before {
+    content: "\f124";
+}
+
+.ion-chevron-right:before {
+    content: "\f125";
+}
+
+.ion-pin:before {
+    content: "\f3a3";
+}
+
+.ion-wifi:before {
+    content: "\f25c";
+}
+
+.ion-eye:before {
+    content: "\f133";
+}
+
+.ion-ios-arrow-thin-left:before {
+    content: "\f3d5";
+}
+
+.ion-ios-arrow-thin-right:before {
+    content: "\f3d6";
+}
+
+.ion-arrow-up-b:before {
+    content: "\f10d";
+}
+
+.ion-arrow-down-b:before {
+    content: "\f104";
+}
+
+.ion-android-locate:before {
+    content: "\f2e9";
+}
+
+.ion-android-close:before {
+    content: "\f2d7";
+}
+
+.ion-android-lock:before {
+    content: "\f392";
+}
+
+.ion-ios-copy:before {
+    content: "\f41c";
+}
+
+.ion-location:before {
+    content: "\f456";
+}
+
+.ion-android-remove:before {
+    content: "\f2f4";
+}
+
+.ion-ios-person:before {
+    content: "\f47e";
+}
diff --git a/bower.json b/bower.json
index be1795a..e8d473a 100644
--- a/bower.json
+++ b/bower.json
@@ -11,7 +11,6 @@
     "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",
diff --git a/html/index.html b/html/index.html
index cc95aa5..382ec6f 100644
--- a/html/index.html
+++ b/html/index.html
@@ -3,7 +3,7 @@
 <head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, user-scalable=no">
-  <link rel="stylesheet" href="css/ionicons.min.css">
+  <link rel="stylesheet" href="hopglass-icons.css">
   <link rel="stylesheet" href="roboto-slab-fontface.css">
   <link rel="stylesheet" href="roboto-fontface.css">
   <link rel="stylesheet" href="style.css">
diff --git a/index.html b/index.html
index 96935bc..cc6f536 100644
--- a/index.html
+++ b/index.html
@@ -7,7 +7,7 @@
   <link rel="stylesheet" href="bower_components/roboto-fontface/roboto-fontface.css">
   <link rel="stylesheet" href="bower_components/leaflet/dist/leaflet.css">
   <link rel="stylesheet" href="bower_components/Leaflet.label/dist/leaflet.label.css">
-  <link rel="stylesheet" href="bower_components/ionicons/css/ionicons.min.css">
+  <link rel="stylesheet" href="assets/hopglass-icons.css">
   <link rel="stylesheet" href="style.css">
   <script src="bower_components/es6-shim/es6-shim.min.js"></script>
   <script src="bower_components/requirejs/require.js" data-main="app"></script>
diff --git a/lib/map.js b/lib/map.js
index ea44443..ff030a0 100644
--- a/lib/map.js
+++ b/lib/map.js
@@ -86,7 +86,7 @@ define(["map/clientlayer", "map/labelslayer",
 
       onAdd: function () {
         var button = L.DomUtil.create("button", "coord-picker");
-        button.textContent = "\uF2A6";
+        button.textContent = "\uF3A3";
 
         // Click propagation isn't disabled as this causes problems with the
         // location picking mode; instead propagation is stopped in onClick().
diff --git a/tasks/build.js b/tasks/build.js
index 2fd1e83..b360a60 100644
--- a/tasks/build.js
+++ b/tasks/build.js
@@ -43,11 +43,11 @@ module.exports = function (grunt) {
       },
       ionicons: {
         src: ["fonts/*",
-          "css/ionicons.min.css"
+          "hopglass-icons.css"
         ],
         expand: true,
         dest: "build/",
-        cwd: "bower_components/ionicons/"
+        cwd: "assets/icons/"
       },
       leafletImages: {
         src: ["images/*"],

From 0a22ed5e6f2af0d2ea8d2350737827506d3d897c Mon Sep 17 00:00:00 2001
From: Xaver Maierhofer <xaver.maierhofer@xwissen.info>
Date: Sun, 22 May 2016 15:40:13 +0200
Subject: [PATCH 04/32] [TASK] Only include additional german locale English is
 default

---
 app.js                | 4 +++-
 lib/infobox/node.js   | 2 +-
 lib/main.js           | 2 +-
 lib/map.js            | 2 +-
 lib/simplenodelist.js | 2 +-
 5 files changed, 7 insertions(+), 5 deletions(-)

diff --git a/app.js b/app.js
index ef131ef..22d04c7 100644
--- a/app.js
+++ b/app.js
@@ -5,7 +5,8 @@ require.config({
     "leaflet.label": "../bower_components/Leaflet.label/dist/leaflet.label",
     "leaflet.providers": "../bower_components/leaflet-providers/leaflet-providers",
     "chroma-js": "../bower_components/chroma-js/chroma.min",
-    "moment": "../bower_components/moment/min/moment-with-locales.min",
+    "moment": "../bower_components/moment/min/moment.min",
+    "moment.de": "../bower_components/moment/locale/de",
     "tablesort": "../bower_components/tablesort/tablesort.min",
     "tablesort.numeric": "../bower_components/tablesort/src/sorts/tablesort.numeric",
     "d3": "../bower_components/d3/d3.min",
@@ -19,6 +20,7 @@ require.config({
   shim: {
     "leaflet.label": ["leaflet"],
     "leaflet.providers": ["leaflet"],
+    "moment.de": ["moment"],
     "tablesort": {
       exports: "Tablesort"
     },
diff --git a/lib/infobox/node.js b/lib/infobox/node.js
index 3654c3d..21e0167 100644
--- a/lib/infobox/node.js
+++ b/lib/infobox/node.js
@@ -1,4 +1,4 @@
-define(["moment", "numeral", "tablesort", "tablesort.numeric"],
+define(["moment", "numeral", "tablesort", "tablesort.numeric", "moment.de"],
   function (moment, numeral, Tablesort) {
     function showGeoURI(d) {
       function showLatitude(d) {
diff --git a/lib/main.js b/lib/main.js
index 8305891..bfb3637 100644
--- a/lib/main.js
+++ b/lib/main.js
@@ -1,4 +1,4 @@
-define(["moment", "router", "leaflet", "gui", "numeral"],
+define(["moment", "router", "leaflet", "gui", "numeral", "moment.de"],
   function (moment, Router, L, GUI, numeral) {
     return function (config) {
       function handleData(data) {
diff --git a/lib/map.js b/lib/map.js
index ff030a0..57b28b0 100644
--- a/lib/map.js
+++ b/lib/map.js
@@ -1,6 +1,6 @@
 define(["map/clientlayer", "map/labelslayer",
     "d3", "leaflet", "moment", "locationmarker", "rbush",
-    "leaflet.label", "leaflet.providers"],
+    "leaflet.label", "leaflet.providers", "moment.de"],
   function (ClientLayer, LabelsLayer, d3, L, moment, LocationMarker, rbush) {
     var options = {
       worldCopyJump: true,
diff --git a/lib/simplenodelist.js b/lib/simplenodelist.js
index a327203..bac6c25 100644
--- a/lib/simplenodelist.js
+++ b/lib/simplenodelist.js
@@ -1,4 +1,4 @@
-define(["moment", "virtual-dom"], function (moment, V) {
+define(["moment", "virtual-dom", "moment.de"], function (moment, V) {
   return function (nodes, field, router, title) {
     var self = this;
     var el, tbody;

From f7f5744a7832b116fd1987ca6ef0b0e6a43219e6 Mon Sep 17 00:00:00 2001
From: Xaver Maierhofer <xaver.maierhofer@xwissen.info>
Date: Sun, 22 May 2016 15:58:09 +0200
Subject: [PATCH 05/32] [TASK] Update bower components

---
 Gruntfile.js        | 18 +-----------------
 app.js              |  4 ++--
 bower.json          | 23 +++++++++++------------
 lib/gui.js          |  2 +-
 lib/infobox/node.js |  2 +-
 package.json        | 29 ++++++++++++++---------------
 tasks/build.js      |  5 -----
 7 files changed, 30 insertions(+), 53 deletions(-)

diff --git a/Gruntfile.js b/Gruntfile.js
index 6fcc054..1e79651 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -1,23 +1,7 @@
 module.exports = function (grunt) {
-  grunt.loadNpmTasks("grunt-git-describe");
-
-  grunt.initConfig({
-    "git-describe": {
-      options: {},
-      default: {}
-    }
-  });
-
-  grunt.registerTask("saveRevision", function () {
-    grunt.event.once("git-describe", function (rev) {
-      grunt.option("gitRevision", rev);
-    });
-    grunt.task.run("git-describe");
-  });
-
   grunt.loadTasks("tasks");
 
-  grunt.registerTask("default", ["bower-install-simple", "lint", "saveRevision", "copy", "sass", "postcss", "requirejs"]);
+  grunt.registerTask("default", ["bower-install-simple", "lint", "copy", "sass", "postcss", "requirejs"]);
   grunt.registerTask("lint", ["eslint"]);
   grunt.registerTask("dev", ["default", "connect:server", "watch"]);
 };
diff --git a/app.js b/app.js
index 22d04c7..8ae6bb1 100644
--- a/app.js
+++ b/app.js
@@ -8,7 +8,7 @@ require.config({
     "moment": "../bower_components/moment/min/moment.min",
     "moment.de": "../bower_components/moment/locale/de",
     "tablesort": "../bower_components/tablesort/tablesort.min",
-    "tablesort.numeric": "../bower_components/tablesort/src/sorts/tablesort.numeric",
+    "tablesort.number": "../bower_components/tablesort/src/sorts/tablesort.number",
     "d3": "../bower_components/d3/d3.min",
     "numeral": "../bower_components/numeraljs/min/numeral.min",
     "numeral-intl": "../bower_components/numeraljs/min/languages.min",
@@ -28,7 +28,7 @@ require.config({
       deps: ["numeral"],
       exports: "numeral"
     },
-    "tablesort.numeric": ["tablesort"],
+    "tablesort.number": ["tablesort"],
     "helper": ["numeral-intl"]
   }
 });
diff --git a/bower.json b/bower.json
index e8d473a..e63962c 100644
--- a/bower.json
+++ b/bower.json
@@ -9,21 +9,20 @@
   ],
   "dependencies": {
     "Leaflet.label": "~0.2.1",
-    "chroma-js": "~0.6.1",
-    "leaflet": "~0.7.3",
-    "moment": "~2.9.0",
-    "requirejs": "~2.1.16",
-    "tablesort": "https://github.com/tristen/tablesort.git#v3.0.2",
+    "chroma-js": "~1.1.1",
+    "leaflet": "~0.7.7",
+    "moment": "~2.13.0",
+    "requirejs": "~2.2.0",
+    "tablesort": "https://github.com/tristen/tablesort.git#v4.0.1",
     "roboto-slab-fontface": "*",
-    "es6-shim": "~0.27.1",
-    "almond": "~0.3.1",
-    "r.js": "~2.1.16",
-    "d3": "~3.5.5",
+    "es6-shim": "~0.35.1",
+    "almond": "~0.3.2",
+    "d3": "~3.5.17",
     "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",
+    "virtual-dom": "~2.1.1",
+    "leaflet-providers": "~1.1.10",
+    "rbush": "https://github.com/mourner/rbush.git#~1.4.3",
     "jshashes": "~1.0.5"
   },
   "authors": [
diff --git a/lib/gui.js b/lib/gui.js
index f757e29..f8427ed 100644
--- a/lib/gui.js
+++ b/lib/gui.js
@@ -10,7 +10,7 @@ define(["chroma-js", "map", "sidebar", "tabs", "container", "meshstats",
       var content;
       var contentDiv;
 
-      var linkScale = chroma.scale(chroma.interpolate.bezier(["#04C714", "#FF5500", "#F02311"])).domain([1, 5]);
+      var linkScale = chroma.scale(chroma.bezier(["#04C714", "#FF5500", "#F02311"])).domain([1, 5]);
       var sidebar;
 
       var buttons = document.createElement("div");
diff --git a/lib/infobox/node.js b/lib/infobox/node.js
index 21e0167..3710272 100644
--- a/lib/infobox/node.js
+++ b/lib/infobox/node.js
@@ -1,4 +1,4 @@
-define(["moment", "numeral", "tablesort", "tablesort.numeric", "moment.de"],
+define(["moment", "numeral", "tablesort", "tablesort.number", "moment.de"],
   function (moment, numeral, Tablesort) {
     function showGeoURI(d) {
       function showLatitude(d) {
diff --git a/package.json b/package.json
index d5cbd37..98738ec 100644
--- a/package.json
+++ b/package.json
@@ -4,21 +4,20 @@
     "test": "node -e \"require('grunt').cli()\" '' clean lint"
   },
   "devDependencies": {
-    "autoprefixer": "^6.3.3",
-    "grunt": "^0.4.5",
-    "grunt-check-dependencies": "^0.6.0",
-    "grunt-contrib-clean": "^0.6.0",
-    "grunt-contrib-connect": "^0.8.0",
-    "grunt-contrib-copy": "^0.5.0",
-    "grunt-contrib-cssmin": "^0.12.2",
-    "grunt-contrib-requirejs": "^0.4.4",
-    "grunt-sass": "^1.1.0",
-    "grunt-postcss": "^0.7.2",
-    "grunt-contrib-uglify": "^0.5.1",
-    "grunt-contrib-watch": "^0.6.1",
-    "grunt-eslint": "^10.0.0",
-    "grunt-bower-install-simple": "^1.1.2",
-    "grunt-git-describe": "^2.3.2"
+    "autoprefixer": "^6.3.6",
+    "grunt": "^1.0.1",
+    "grunt-bower-install-simple": "^1.2.1",
+    "grunt-check-dependencies": "^0.12.0",
+    "grunt-contrib-clean": "^1.0.0",
+    "grunt-contrib-connect": "^1.0.2",
+    "grunt-contrib-copy": "^1.0.0",
+    "grunt-contrib-cssmin": "^1.0.1",
+    "grunt-contrib-requirejs": "^1.0.0",
+    "grunt-contrib-uglify": "^1.0.1",
+    "grunt-contrib-watch": "^1.0.0",
+    "grunt-eslint": "^18.1.0",
+    "grunt-postcss": "^0.8.0",
+    "grunt-sass": "^1.2.0"
   },
   "eslintConfig": {
     "env": {
diff --git a/tasks/build.js b/tasks/build.js
index b360a60..388904b 100644
--- a/tasks/build.js
+++ b/tasks/build.js
@@ -3,11 +3,6 @@ module.exports = function (grunt) {
     bowerdir: "bower_components",
     copy: {
       html: {
-        options: {
-          process: function (content) {
-            return content.replace("#revision#", grunt.option("gitRevision"));
-          }
-        },
         src: ["*.html"],
         expand: true,
         cwd: "html/",

From 30b6f84b6d4246d9bb103173f24b6a25cf9e3e2a Mon Sep 17 00:00:00 2001
From: Xaver Maierhofer <xaver.maierhofer@xwissen.info>
Date: Sun, 22 May 2016 18:51:42 +0200
Subject: [PATCH 06/32] [BUGFIX] Correct css path to icon font

---
 index.html | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/index.html b/index.html
index cc6f536..eb6c970 100644
--- a/index.html
+++ b/index.html
@@ -7,7 +7,7 @@
   <link rel="stylesheet" href="bower_components/roboto-fontface/roboto-fontface.css">
   <link rel="stylesheet" href="bower_components/leaflet/dist/leaflet.css">
   <link rel="stylesheet" href="bower_components/Leaflet.label/dist/leaflet.label.css">
-  <link rel="stylesheet" href="assets/hopglass-icons.css">
+  <link rel="stylesheet" href="assets/icons/hopglass-icons.css">
   <link rel="stylesheet" href="style.css">
   <script src="bower_components/es6-shim/es6-shim.min.js"></script>
   <script src="bower_components/requirejs/require.js" data-main="app"></script>

From 44571c51f03d4ce8f75ed45e973ac38e5694926f Mon Sep 17 00:00:00 2001
From: Xaver Maierhofer <xaver.maierhofer@xwissen.info>
Date: Sun, 22 May 2016 22:13:46 +0200
Subject: [PATCH 07/32] [TASK] Inline style and icon css and es6-shim

---
 Gruntfile.js    |  2 +-
 html/index.html |  6 +++---
 package.json    |  1 +
 tasks/build.js  | 11 +++++++++++
 4 files changed, 16 insertions(+), 4 deletions(-)

diff --git a/Gruntfile.js b/Gruntfile.js
index 1e79651..2c15b33 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -1,7 +1,7 @@
 module.exports = function (grunt) {
   grunt.loadTasks("tasks");
 
-  grunt.registerTask("default", ["bower-install-simple", "lint", "copy", "sass", "postcss", "requirejs"]);
+  grunt.registerTask("default", ["bower-install-simple", "lint", "copy", "sass", "postcss", "requirejs", "inline"]);
   grunt.registerTask("lint", ["eslint"]);
   grunt.registerTask("dev", ["default", "connect:server", "watch"]);
 };
diff --git a/html/index.html b/html/index.html
index 382ec6f..ff5fa76 100644
--- a/html/index.html
+++ b/html/index.html
@@ -3,11 +3,11 @@
 <head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, user-scalable=no">
-  <link rel="stylesheet" href="hopglass-icons.css">
+  <link rel="stylesheet" href="hopglass-icons.css?__inline=true">
   <link rel="stylesheet" href="roboto-slab-fontface.css">
   <link rel="stylesheet" href="roboto-fontface.css">
-  <link rel="stylesheet" href="style.css">
-  <script src="vendor/es6-shim/es6-shim.min.js"></script>
+  <link rel="stylesheet" href="style.css?__inline=true">
+  <script src="vendor/es6-shim/es6-shim.min.js?__inline=true"></script>
   <script src="app.js"></script>
   <script>
     console.log("Version: #revision#");
diff --git a/package.json b/package.json
index 98738ec..2ffe259 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
     "grunt-contrib-uglify": "^1.0.1",
     "grunt-contrib-watch": "^1.0.0",
     "grunt-eslint": "^18.1.0",
+    "grunt-inline": "^0.3.6",
     "grunt-postcss": "^0.8.0",
     "grunt-sass": "^1.2.0"
   },
diff --git a/tasks/build.js b/tasks/build.js
index 388904b..54cad5a 100644
--- a/tasks/build.js
+++ b/tasks/build.js
@@ -85,6 +85,16 @@ module.exports = function (grunt) {
         }
       }
     },
+    inline: {
+      dist: {
+        options: {
+          cssmin: true,
+          uglify: true
+        },
+        src: "build/index.html",
+        dest: "build/index.html"
+      }
+    },
     "bower-install-simple": {
       options: {
         directory: "<%=bowerdir%>",
@@ -118,4 +128,5 @@ module.exports = function (grunt) {
   grunt.loadNpmTasks("grunt-contrib-requirejs");
   grunt.loadNpmTasks("grunt-sass");
   grunt.loadNpmTasks("grunt-postcss");
+  grunt.loadNpmTasks("grunt-inline");
 };

From 8bb0da9d878032faae78fa3e17f87fa92785db63 Mon Sep 17 00:00:00 2001
From: Xaver Maierhofer <xaver.maierhofer@xwissen.info>
Date: Sun, 22 May 2016 23:32:05 +0200
Subject: [PATCH 08/32] [TASK] CGL - Optimize returns and unnecessary math

---
 lib/forcegraph.js      | 8 +++-----
 lib/map.js             | 4 +---
 lib/map/labelslayer.js | 4 +---
 3 files changed, 5 insertions(+), 11 deletions(-)

diff --git a/lib/forcegraph.js b/lib/forcegraph.js
index b9a998c..d1a24d3 100644
--- a/lib/forcegraph.js
+++ b/lib/forcegraph.js
@@ -273,8 +273,7 @@ define(["d3"], function (d3) {
 
     function drawNode(color, radius, scale, r) {
       var node = document.createElement("canvas");
-      node.width = scale * radius * 8 * r;
-      node.height = node.width;
+      node.height = node.width = scale * radius * 8 * r;
 
       var nctx = node.getContext("2d");
       nctx.scale(scale * r, scale * r);
@@ -288,12 +287,12 @@ define(["d3"], function (d3) {
       nctx.arc(0, 0, radius, 0, 2 * Math.PI);
 
       nctx.strokeStyle = "rgba(255, 0, 0, 1)";
-      nctx.shadowOffsetX = node.width * 1.5 + 0;
+      nctx.shadowOffsetX = node.width * 1.5;
       nctx.shadowOffsetY = node.height * 1.5 + 3;
       nctx.shadowBlur = 12;
       nctx.shadowColor = "rgba(0, 0, 0, 0.16)";
       nctx.stroke();
-      nctx.shadowOffsetX = node.width * 1.5 + 0;
+      nctx.shadowOffsetX = node.width * 1.5;
       nctx.shadowOffsetY = node.height * 1.5 + 3;
       nctx.shadowBlur = 12;
       nctx.shadowColor = "rgba(0, 0, 0, 0.23)";
@@ -562,7 +561,6 @@ define(["d3"], function (d3) {
 
       if (links.length > 0) {
         router.link(links[0].o)();
-
       }
     }
 
diff --git a/lib/map.js b/lib/map.js
index 57b28b0..c54cf55 100644
--- a/lib/map.js
+++ b/lib/map.js
@@ -135,7 +135,7 @@ define(["map/clientlayer", "map/labelslayer",
         return "distance" in d && d.type !== "VPN";
       });
 
-      var lines = graph.map(function (d) {
+      return graph.map(function (d) {
         var opts = {
           color: d.type === "Kabel" ? "#50B0F0" : linkScale(d.tq).hex(),
           weight: 4,
@@ -156,8 +156,6 @@ define(["map/clientlayer", "map/labelslayer",
 
         return line;
       });
-
-      return lines;
     }
 
     var iconOnline = {
diff --git a/lib/map/labelslayer.js b/lib/map/labelslayer.js
index e616877..a6af979 100644
--- a/lib/map/labelslayer.js
+++ b/lib/map/labelslayer.js
@@ -73,7 +73,7 @@ define(["leaflet", "rbush"],
       return [x, y, x + width, y + height];
     }
 
-    var c = L.TileLayer.Canvas.extend({
+    return L.TileLayer.Canvas.extend({
       onAdd: function (map) {
         L.TileLayer.Canvas.prototype.onAdd.call(this, map);
         if (this.data) {
@@ -235,6 +235,4 @@ define(["leaflet", "rbush"],
         }).forEach(drawLabel);
       }
     });
-
-    return c;
   });

From 88f43bc57cecdca6a7a5fa1a14699fe6413f695d Mon Sep 17 00:00:00 2001
From: Xaver Maierhofer <xaver.maierhofer@xwissen.info>
Date: Sun, 22 May 2016 23:39:19 +0200
Subject: [PATCH 09/32] [TASK] Add index option to grunt-conncet server

---
 tasks/development.js | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/tasks/development.js b/tasks/development.js
index 2c00cd7..3ae384e 100644
--- a/tasks/development.js
+++ b/tasks/development.js
@@ -3,7 +3,12 @@ module.exports = function (grunt) {
     connect: {
       server: {
         options: {
-          base: "build/", //TODO: once grunt-contrib-connect 0.9 is released, set index file
+          base: {
+            path: 'build',
+            options: {
+              index: 'index.html'
+            }
+          },
           livereload: true
         }
       }

From 231bfcedd7ef065b5ef83d08d67877e8b6787eb9 Mon Sep 17 00:00:00 2001
From: Xaver Maierhofer <xaver.maierhofer@xwissen.info>
Date: Mon, 23 May 2016 00:13:21 +0200
Subject: [PATCH 10/32] [TASK] Seperate dev and default for requirejs

---
 Gruntfile.js         |  5 +++--
 README.md            |  4 ++++
 tasks/build.js       | 18 ++++++++++++++----
 tasks/development.js |  2 +-
 4 files changed, 22 insertions(+), 7 deletions(-)

diff --git a/Gruntfile.js b/Gruntfile.js
index 2c15b33..99fcd04 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -1,8 +1,9 @@
 module.exports = function (grunt) {
   grunt.loadTasks("tasks");
 
-  grunt.registerTask("default", ["bower-install-simple", "lint", "copy", "sass", "postcss", "requirejs", "inline"]);
+  grunt.registerTask("default", ["bower-install-simple", "lint", "copy", "sass", "postcss", "requirejs:default", "inline"]);
   grunt.registerTask("lint", ["eslint"]);
-  grunt.registerTask("dev", ["default", "connect:server", "watch"]);
+  grunt.registerTask("dev", ["bower-install-simple", "lint", "copy", "sass", "requirejs:dev"]);
+  grunt.registerTask("serve", ["dev", "connect:server", "watch"]);
 };
 
diff --git a/README.md b/README.md
index ae7bba7..4a15dbc 100644
--- a/README.md
+++ b/README.md
@@ -46,6 +46,10 @@ Just run the following command from the hopglass directory:
 
 This will generate `build/` containing all required files.
 
+## Development
+
+Use `grunt serve` for development.
+
 # Configure
 
 Copy `config.json.example` to `build/config.json` and change it to match your community.
diff --git a/tasks/build.js b/tasks/build.js
index 54cad5a..9c62838 100644
--- a/tasks/build.js
+++ b/tasks/build.js
@@ -109,15 +109,25 @@ module.exports = function (grunt) {
       }
     },
     requirejs: {
-      compile: {
+      default: {
         options: {
           baseUrl: "lib",
           name: "../bower_components/almond/almond",
           mainConfigFile: "app.js",
           include: "../app",
-          wrap: true,
-          optimize: "uglify",
-          out: "build/app.js"
+          out: "build/app.js",
+          build: false
+        }
+      },
+      dev: {
+        options: {
+          baseUrl: "lib",
+          name: "../bower_components/almond/almond",
+          mainConfigFile: "app.js",
+          include: "../app",
+          optimize: "none",
+          out: "build/app.js",
+          build: false
         }
       }
     }
diff --git a/tasks/development.js b/tasks/development.js
index 3ae384e..02826b0 100644
--- a/tasks/development.js
+++ b/tasks/development.js
@@ -19,7 +19,7 @@ module.exports = function (grunt) {
           livereload: true
         },
         files: ["*.css", "app.js", "lib/**/*.js", "*.html"],
-        tasks: ["default"]
+        tasks: ["dev"]
       },
       config: {
         options: {

From 7f3a86596ade6a3002054d983e680528d2adeb6d Mon Sep 17 00:00:00 2001
From: Xaver Maierhofer <xaver.maierhofer@xwissen.info>
Date: Mon, 23 May 2016 00:48:03 +0200
Subject: [PATCH 11/32] [TASK] Replace all specialchars in NODE_NAME in stat
 urls

---
 lib/infobox/node.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/infobox/node.js b/lib/infobox/node.js
index 3710272..68137fc 100644
--- a/lib/infobox/node.js
+++ b/lib/infobox/node.js
@@ -469,7 +469,7 @@ define(["moment", "numeral", "tablesort", "tablesort.number", "moment.de"],
     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";
+      subst["{NODE_NAME}"] = d.nodeinfo.hostname ? d.nodeinfo.hostname.replace(/[^a-z0-9\-]/ig, '_') : "unknown";
       return showStat(o, subst);
     }
 

From b510dda00613daf7107365e5338e973c5767eb57 Mon Sep 17 00:00:00 2001
From: Xaver Maierhofer <xmaierhofer@1drop.de>
Date: Mon, 23 May 2016 09:30:19 +0200
Subject: [PATCH 12/32] [BUGFIX] Enable build for default

---
 tasks/build.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tasks/build.js b/tasks/build.js
index 9c62838..3a70fe8 100644
--- a/tasks/build.js
+++ b/tasks/build.js
@@ -116,7 +116,7 @@ module.exports = function (grunt) {
           mainConfigFile: "app.js",
           include: "../app",
           out: "build/app.js",
-          build: false
+          build: true
         }
       },
       dev: {

From d367bf58bff711055ef99308062dc1c5fef51399 Mon Sep 17 00:00:00 2001
From: Xaver Maierhofer <xaver.maierhofer@xwissen.info>
Date: Tue, 24 May 2016 00:22:34 +0200
Subject: [PATCH 13/32] [TASK] Unicode instead of utf8 icons

---
 lib/infobox/link.js | 3 ++-
 lib/infobox/node.js | 7 ++++---
 2 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/lib/infobox/link.js b/lib/infobox/link.js
index 2d5e8c0..260931f 100644
--- a/lib/infobox/link.js
+++ b/lib/infobox/link.js
@@ -18,7 +18,8 @@ define(function () {
     }
     a1.textContent = unknown ? d.source.id : d.source.node.nodeinfo.hostname;
     h2.appendChild(a1);
-    h2.appendChild(document.createTextNode(" → "));
+    h2.appendChild(document.createTextNode(" \uF3D6 "));
+    h2.className = 'ion-inside';
     var a2 = document.createElement("a");
     a2.href = "#";
     a2.onclick = router.node(d.target.node);
diff --git a/lib/infobox/node.js b/lib/infobox/node.js
index 68137fc..eccc466 100644
--- a/lib/infobox/node.js
+++ b/lib/infobox/node.js
@@ -156,12 +156,12 @@ define(["moment", "numeral", "tablesort", "tablesort.number", "moment.de"],
 
         var span = document.createElement("span");
         span.classList.add("clients");
-        span.textContent = " ".repeat(d.statistics.clients);
+        span.textContent = "\uF47E ".repeat(d.statistics.clients);
         el.appendChild(span);
 
         var spanmesh = document.createElement("span");
         spanmesh.classList.add("clientsMesh");
-        spanmesh.textContent = " ".repeat(meshclients - d.statistics.clients);
+        spanmesh.textContent = "\uF47E ".repeat(meshclients - d.statistics.clients);
         el.appendChild(spanmesh);
       };
     }
@@ -577,7 +577,8 @@ define(["moment", "numeral", "tablesort", "tablesort.number", "moment.de"],
           var tr = document.createElement("tr");
 
           var td1 = document.createElement("td");
-          td1.appendChild(document.createTextNode(d.incoming ? " ← " : " → "));
+          td1.className = 'ion-inside';
+          td1.appendChild(document.createTextNode(d.incoming ? " \uF3D5 " : " \uF3D6 "));
           tr.appendChild(td1);
 
           var td2 = document.createElement("td");

From 136157eb37b3f885045d5b8c05e51408214636cb Mon Sep 17 00:00:00 2001
From: Xaver Maierhofer <xaver.maierhofer@xwissen.info>
Date: Tue, 24 May 2016 00:23:35 +0200
Subject: [PATCH 14/32] [TASK] Only add german language from numeral.js

---
 app.js             | 6 +++---
 lib/proportions.js | 2 +-
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/app.js b/app.js
index 8ae6bb1..f08f023 100644
--- a/app.js
+++ b/app.js
@@ -11,7 +11,7 @@ require.config({
     "tablesort.number": "../bower_components/tablesort/src/sorts/tablesort.number",
     "d3": "../bower_components/d3/d3.min",
     "numeral": "../bower_components/numeraljs/min/numeral.min",
-    "numeral-intl": "../bower_components/numeraljs/min/languages.min",
+    "numeral.de": "../bower_components/numeraljs/min/languages/de.min",
     "virtual-dom": "../bower_components/virtual-dom/dist/virtual-dom",
     "rbush": "../bower_components/rbush/rbush",
     "helper": "../helper",
@@ -24,12 +24,12 @@ require.config({
     "tablesort": {
       exports: "Tablesort"
     },
-    "numeral-intl": {
+    "numeral.de": {
       deps: ["numeral"],
       exports: "numeral"
     },
     "tablesort.number": ["tablesort"],
-    "helper": ["numeral-intl"]
+    "helper": ["numeral.de"]
   }
 });
 
diff --git a/lib/proportions.js b/lib/proportions.js
index 8523165..9d9b2e7 100644
--- a/lib/proportions.js
+++ b/lib/proportions.js
@@ -1,4 +1,4 @@
-define(["chroma-js", "virtual-dom", "numeral-intl", "filters/genericnode", "vercomp"],
+define(["chroma-js", "virtual-dom", "numeral.de", "filters/genericnode", "vercomp"],
   function (Chroma, V, numeral, Filter, vercomp) {
 
     return function (config, filterManager) {

From 6908e2953fbd1fb92e9ad73c0fc02734c3256738 Mon Sep 17 00:00:00 2001
From: Xaver Maierhofer <xaver.maierhofer@xwissen.info>
Date: Tue, 24 May 2016 23:26:46 +0200
Subject: [PATCH 15/32] [TASK] Format load average

---
 lib/infobox/node.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/infobox/node.js b/lib/infobox/node.js
index eccc466..659c596 100644
--- a/lib/infobox/node.js
+++ b/lib/infobox/node.js
@@ -297,7 +297,7 @@ define(["moment", "numeral", "tablesort", "tablesort.number", "moment.de"],
       }
 
       return function (el) {
-        el.appendChild(showLoadBar("load-avg", d.statistics.loadavg));
+        el.appendChild(showLoadBar("load-avg", numeral(d.statistics.loadavg).format("0.00")));
       };
     }
 

From 8520abf6765efaecc0280ba18308faa88fb81084 Mon Sep 17 00:00:00 2001
From: Nils Schneider <nils@nilsschneider.net>
Date: Mon, 29 Feb 2016 14:34:37 +0100
Subject: [PATCH 16/32] [TASK] remove build.js, it is not needed anymore

---
 build.js | 9 ---------
 1 file changed, 9 deletions(-)
 delete mode 100644 build.js

diff --git a/build.js b/build.js
deleted file mode 100644
index 41f588b..0000000
--- a/build.js
+++ /dev/null
@@ -1,9 +0,0 @@
-({
-  baseUrl: "lib",
-  name: "../bower_components/almond/almond",
-  mainConfigFile: "app.js",
-  include: "../app",
-  wrap: true,
-  optimize: "uglify",
-  out: "app-combined.js"
-});

From f1e9aacdf7fe95458c691b70a5968b283bca669d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Milan=20P=C3=A4ssler?= <me@petabyteboy.de>
Date: Fri, 17 Mar 2017 03:19:03 +0100
Subject: [PATCH 17/32] fix linting

---
 lib/map.js       |  2 +-
 tasks/linting.js | 13 +++++--------
 2 files changed, 6 insertions(+), 9 deletions(-)

diff --git a/lib/map.js b/lib/map.js
index c54cf55..890492b 100644
--- a/lib/map.js
+++ b/lib/map.js
@@ -311,7 +311,7 @@ define(["map/clientlayer", "map/labelslayer",
             localStorage.setItem("map/customLayers", JSON.stringify(Object.keys(customLayers)));
           }
         } catch (e) {
-
+          console.error(e);
         }
       }
 
diff --git a/tasks/linting.js b/tasks/linting.js
index 71311dc..de1b8d5 100644
--- a/tasks/linting.js
+++ b/tasks/linting.js
@@ -13,15 +13,12 @@ module.exports = function (grunt) {
     },
     eslint: {
       options: {
+        extends: [
+          "defaults/configurations/eslint"
+	],
         rules: {
-          "semi": [2, "never"],
-          "curly": [2, "multi"],
-          "strict": [2, "never"],
-          "no-multi-spaces": 0,
-          "no-new": 0,
-          "no-shadow": 0,
-          "no-use-before-define": [1, "nofunc"],
-          "no-underscore-dangle": 0
+          "semi": [2, "always"],
+          "no-undef": 0
         }
       },
       sources: {

From 41c6a03e6b96b34e63c6a4dfad743976eec6a47e Mon Sep 17 00:00:00 2001
From: Xaver Maierhofer <xaver.maierhofer@xwissen.info>
Date: Wed, 25 May 2016 22:52:29 +0200
Subject: [PATCH 18/32] [TASK] Adjust marker style - focus on online nodes

---
 lib/map.js | 14 ++++++++------
 1 file changed, 8 insertions(+), 6 deletions(-)

diff --git a/lib/map.js b/lib/map.js
index 890492b..7269023 100644
--- a/lib/map.js
+++ b/lib/map.js
@@ -179,7 +179,7 @@ define(["map/clientlayer", "map/labelslayer",
     var iconLost = {
       color: "#D43E2A",
       fillColor: "#D43E2A",
-      radius: 6,
+      radius: 4,
       fillOpacity: 0.8,
       opacity: 0.8,
       weight: 1,
@@ -188,11 +188,11 @@ define(["map/clientlayer", "map/labelslayer",
     var iconAlert = {
       color: "#D43E2A",
       fillColor: "#D43E2A",
-      radius: 6,
+      radius: 5,
       fillOpacity: 0.8,
       opacity: 0.8,
       weight: 2,
-      className: "stroke-first node-alert"
+      className: "stroke-first"
     };
     var iconNew = {color: "#1566A9", fillColor: "#93E929", radius: 6, fillOpacity: 1.0, opacity: 0.5, weight: 2};
 
@@ -443,7 +443,7 @@ define(["map/clientlayer", "map/labelslayer",
             m = linkDict[highlight.o.id];
 
             if (m) {
-              m.setStyle({weight: 7, opacity: 1, dashArray: "10, 10"});
+              m.setStyle({weight: 4, opacity: 1, dashArray: "5, 10"});
             }
           }
         }
@@ -560,12 +560,14 @@ define(["map/clientlayer", "map/labelslayer",
               return iconAlert;
             }
 
-            return iconLost;
+            if (d.lastseen.isAfter(moment(data.now).subtract(14, "days"))) {
+              return iconLost;
+            }
           }, router));
 
         groupOffline = L.featureGroup(markersOffline).addTo(map);
-        groupOnline = L.featureGroup(markersOnline).addTo(map);
         groupLost = L.featureGroup(markersLost).addTo(map);
+        groupOnline = L.featureGroup(markersOnline).addTo(map);
         groupNew = L.featureGroup(markersNew).addTo(map);
 
         var rtreeOnlineAll = rbush(9);

From d5aa447ab8322783e31617496e81ebfc202809df Mon Sep 17 00:00:00 2001
From: Xaver Maierhofer <xaver.maierhofer@xwissen.info>
Date: Wed, 25 May 2016 23:34:09 +0200
Subject: [PATCH 19/32] [TASK] Add helper.js to grunt watch

---
 tasks/development.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tasks/development.js b/tasks/development.js
index 02826b0..933f123 100644
--- a/tasks/development.js
+++ b/tasks/development.js
@@ -18,7 +18,7 @@ module.exports = function (grunt) {
         options: {
           livereload: true
         },
-        files: ["*.css", "app.js", "lib/**/*.js", "*.html"],
+        files: ["*.css", "app.js", "helper.js", "lib/**/*.js", "*.html"],
         tasks: ["dev"]
       },
       config: {

From 27389673435b3c1b8cda4350f0e3fc9b014f3ae4 Mon Sep 17 00:00:00 2001
From: Xaver Maierhofer <xaver.maierhofer@xwissen.info>
Date: Wed, 25 May 2016 23:34:36 +0200
Subject: [PATCH 20/32] [TASK] Update npm bower install module

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 2ffe259..613c552 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
   "devDependencies": {
     "autoprefixer": "^6.3.6",
     "grunt": "^1.0.1",
-    "grunt-bower-install-simple": "^1.2.1",
+    "grunt-bower-install-simple": "^1.2.3",
     "grunt-check-dependencies": "^0.12.0",
     "grunt-contrib-clean": "^1.0.0",
     "grunt-contrib-connect": "^1.0.2",

From fe3ba88887c30318a819c963e7763b24105e5424 Mon Sep 17 00:00:00 2001
From: Xaver Maierhofer <xaver.maierhofer@xwissen.info>
Date: Wed, 25 May 2016 23:38:18 +0200
Subject: [PATCH 21/32] [TASK] Replace numeraljs with toFixed vanilla js

---
 app.js              |  9 +--------
 bower.json          |  1 -
 helper.js           |  4 ++--
 lib/infobox/node.js | 10 +++++-----
 lib/main.js         |  6 ++----
 lib/nodelist.js     |  4 ++--
 lib/proportions.js  |  7 +++----
 7 files changed, 15 insertions(+), 26 deletions(-)

diff --git a/app.js b/app.js
index f08f023..53b3a4b 100644
--- a/app.js
+++ b/app.js
@@ -10,8 +10,6 @@ require.config({
     "tablesort": "../bower_components/tablesort/tablesort.min",
     "tablesort.number": "../bower_components/tablesort/src/sorts/tablesort.number",
     "d3": "../bower_components/d3/d3.min",
-    "numeral": "../bower_components/numeraljs/min/numeral.min",
-    "numeral.de": "../bower_components/numeraljs/min/languages/de.min",
     "virtual-dom": "../bower_components/virtual-dom/dist/virtual-dom",
     "rbush": "../bower_components/rbush/rbush",
     "helper": "../helper",
@@ -24,12 +22,7 @@ require.config({
     "tablesort": {
       exports: "Tablesort"
     },
-    "numeral.de": {
-      deps: ["numeral"],
-      exports: "numeral"
-    },
-    "tablesort.number": ["tablesort"],
-    "helper": ["numeral.de"]
+    "tablesort.number": ["tablesort"]
   }
 });
 
diff --git a/bower.json b/bower.json
index e63962c..ef1fd19 100644
--- a/bower.json
+++ b/bower.json
@@ -18,7 +18,6 @@
     "es6-shim": "~0.35.1",
     "almond": "~0.3.2",
     "d3": "~3.5.17",
-    "numeraljs": "~1.5.3",
     "roboto-fontface": "~0.3.0",
     "virtual-dom": "~2.1.1",
     "leaflet-providers": "~1.1.10",
diff --git a/helper.js b/helper.js
index 1237021..a97b38f 100644
--- a/helper.js
+++ b/helper.js
@@ -118,11 +118,11 @@ function showDistance(d) {
     return;
   }
 
-  return numeral(d.distance).format("0,0") + " m";
+  return d.distance.toFixed(0) + " m"
 }
 
 function showTq(d) {
-  return numeral(1 / d.tq).format("0%");
+  return (1 / d.tq * 100).toFixed(0) + "%"
 }
 
 /* Infobox stuff (XXX: move to module) */
diff --git a/lib/infobox/node.js b/lib/infobox/node.js
index 659c596..421a60d 100644
--- a/lib/infobox/node.js
+++ b/lib/infobox/node.js
@@ -1,5 +1,5 @@
-define(["moment", "numeral", "tablesort", "tablesort.number", "moment.de"],
-  function (moment, numeral, Tablesort) {
+define(["moment", "tablesort", "tablesort.number", "moment.de"],
+  function (moment, Tablesort) {
     function showGeoURI(d) {
       function showLatitude(d) {
         var suffix = Math.sign(d) > -1 ? "' N" : "' S";
@@ -8,7 +8,7 @@ define(["moment", "numeral", "tablesort", "tablesort.number", "moment.de"],
         var min = (d * 60) % 60;
         a = (a < 10 ? "0" : "") + a;
 
-        return a + "° " + numeral(min).format("0.000") + suffix;
+        return a + "° " + min.toFixed(3) + suffix;
       }
 
       function showLongitude(d) {
@@ -18,7 +18,7 @@ define(["moment", "numeral", "tablesort", "tablesort.number", "moment.de"],
         var min = (d * 60) % 60;
         a = (a < 100 ? "0" + (a < 10 ? "0" : "") : "") + a;
 
-        return a + "° " + numeral(min).format("0.000") + suffix;
+        return a + "° " + min.toFixed(3) + suffix;
       }
 
       if (!has_location(d)) {
@@ -297,7 +297,7 @@ define(["moment", "numeral", "tablesort", "tablesort.number", "moment.de"],
       }
 
       return function (el) {
-        el.appendChild(showLoadBar("load-avg", numeral(d.statistics.loadavg).format("0.00")));
+        el.appendChild(showLoadBar("load-avg", d.statistics.loadavg.toFixed(2)));
       };
     }
 
diff --git a/lib/main.js b/lib/main.js
index bfb3637..aa4163a 100644
--- a/lib/main.js
+++ b/lib/main.js
@@ -1,5 +1,5 @@
-define(["moment", "router", "leaflet", "gui", "numeral", "moment.de"],
-  function (moment, Router, L, GUI, numeral) {
+define(["moment", "router", "leaflet", "gui", "moment.de"],
+  function (moment, Router, L, GUI) {
     return function (config) {
       function handleData(data) {
         var dataNodes = {};
@@ -197,8 +197,6 @@ define(["moment", "router", "leaflet", "gui", "numeral", "moment.de"],
           }
         };
       }
-
-      numeral.language("de");
       moment.locale("de");
 
       var router = new Router();
diff --git a/lib/nodelist.js b/lib/nodelist.js
index b81ec59..950bdd3 100644
--- a/lib/nodelist.js
+++ b/lib/nodelist.js
@@ -1,4 +1,4 @@
-define(["sorttable", "virtual-dom", "numeral"], function (SortTable, V, numeral) {
+define(["sorttable", "virtual-dom"], function (SortTable, V) {
   function getUptime(now, d) {
     if (d.flags.online && "uptime" in d.statistics) {
       return Math.round(d.statistics.uptime);
@@ -75,7 +75,7 @@ define(["sorttable", "virtual-dom", "numeral"], function (SortTable, V, numeral)
       var td1 = V.h("td", td1Content);
       var td2 = V.h("td", showUptime(d.uptime));
       var td3 = V.h("td", d.meshlinks.toString());
-      var td4 = V.h("td", numeral("clients" in d.statistics ? d.statistics.clients : "").format("0,0"));
+      var td4 = V.h("td", ("clients" in d.statistics ? d.statistics.clients : 0).toFixed(0));
 
       return V.h("tr", [td1, td2, td3, td4]);
     }
diff --git a/lib/proportions.js b/lib/proportions.js
index 9d9b2e7..e7b6e56 100644
--- a/lib/proportions.js
+++ b/lib/proportions.js
@@ -1,5 +1,5 @@
-define(["chroma-js", "virtual-dom", "numeral.de", "filters/genericnode", "vercomp"],
-  function (Chroma, V, numeral, Filter, vercomp) {
+define(["chroma-js", "virtual-dom", "filters/genericnode", "vercomp"],
+  function (Chroma, V, Filter, vercomp) {
 
     return function (config, filterManager) {
       var self = this;
@@ -80,7 +80,6 @@ define(["chroma-js", "virtual-dom", "numeral.de", "filters/genericnode", "vercom
         });
       }
 
-
       function addFilter(filter) {
         return function () {
           filterManager.addFilter(filter);
@@ -117,7 +116,7 @@ define(["chroma-js", "virtual-dom", "numeral.de", "filters/genericnode", "vercom
               backgroundColor: scale(v).hex(),
               color: c1 > c2 ? "white" : "black"
             }
-          }, numeral(d[1]).format("0,0")));
+          }, d[1].toFixed(0)));
 
           return V.h("tr", [th, td]);
         });

From d4670d8742d208c28cc928d0cb331cfbca798fdb Mon Sep 17 00:00:00 2001
From: Xaver Maierhofer <xaver.maierhofer@xwissen.info>
Date: Wed, 25 May 2016 23:53:46 +0200
Subject: [PATCH 22/32] [TASK] Simple sort for firmware versions

---
 lib/proportions.js |  8 ++++--
 lib/vercomp.js     | 68 ----------------------------------------------
 2 files changed, 5 insertions(+), 71 deletions(-)
 delete mode 100644 lib/vercomp.js

diff --git a/lib/proportions.js b/lib/proportions.js
index e7b6e56..6bf6390 100644
--- a/lib/proportions.js
+++ b/lib/proportions.js
@@ -1,5 +1,5 @@
-define(["chroma-js", "virtual-dom", "filters/genericnode", "vercomp"],
-  function (Chroma, V, Filter, vercomp) {
+define(["chroma-js", "virtual-dom", "filters/genericnode"],
+  function (Chroma, V, Filter) {
 
     return function (config, filterManager) {
       var self = this;
@@ -214,7 +214,9 @@ define(["chroma-js", "virtual-dom", "filters/genericnode", "vercomp"],
           return b[1] - a[1];
         }));
         fillTable("Firmware", fwTable, fwDict.sort(function (a, b) {
-          return vercomp(b[0], a[0]);
+          if(b[0] < a[0]) return -1;
+          if(b[0] > a[0]) return 1;
+          return 0;
         }));
         fillTable("Hardware", hwTable, hwDict.sort(function (a, b) {
           return b[1] - a[1];
diff --git a/lib/vercomp.js b/lib/vercomp.js
deleted file mode 100644
index 4752b11..0000000
--- a/lib/vercomp.js
+++ /dev/null
@@ -1,68 +0,0 @@
-define([], function () {
-  function order(c) {
-    if (/^\d$/.test(c)) {
-      return 0;
-    } else if (/^[a-z]$/i.test(c)) {
-      return c.charCodeAt(0);
-    } else if (c === "~") {
-      return -1;
-    } else if (c) {
-      return c.charCodeAt(0) + 256;
-    } else {
-      return 0;
-    }
-  }
-
-  // Based on dpkg code
-  function vercomp(a, b) {
-    var apos = 0, bpos = 0;
-    while (apos < a.length || bpos < b.length) {
-      var firstDiff = 0;
-
-      while ((apos < a.length && !/^\d$/.test(a[apos])) || (bpos < b.length && !/^\d$/.test(b[bpos]))) {
-        var ac = order(a[apos]);
-        var bc = order(b[bpos]);
-
-        if (ac !== bc) {
-          return ac - bc;
-        }
-
-        apos++;
-        bpos++;
-      }
-
-      while (a[apos] === "0") {
-        apos++;
-      }
-
-      while (b[bpos] === "0") {
-        bpos++;
-      }
-
-      while (/^\d$/.test(a[apos]) && /^\d$/.test(b[bpos])) {
-        if (firstDiff === 0) {
-          firstDiff = a.charCodeAt(apos) - b.charCodeAt(bpos);
-        }
-
-        apos++;
-        bpos++;
-      }
-
-      if (/^\d$/.test(a[apos])) {
-        return 1;
-      }
-
-      if (/^\d$/.test(b[bpos])) {
-        return -1;
-      }
-
-      if (firstDiff !== 0) {
-        return firstDiff;
-      }
-    }
-
-    return 0;
-  }
-
-  return vercomp;
-});

From 09714f3b58f3a0c79c90aec71ae34fe1328d1921 Mon Sep 17 00:00:00 2001
From: Xaver Maierhofer <xaver.maierhofer@xwissen.info>
Date: Thu, 26 May 2016 00:27:46 +0200
Subject: [PATCH 23/32] [TASK] Remove jsHashes

---
 app.js                 | 3 +--
 bower.json             | 3 +--
 lib/map/clientlayer.js | 8 +++-----
 3 files changed, 5 insertions(+), 9 deletions(-)

diff --git a/app.js b/app.js
index 53b3a4b..8720290 100644
--- a/app.js
+++ b/app.js
@@ -12,8 +12,7 @@ require.config({
     "d3": "../bower_components/d3/d3.min",
     "virtual-dom": "../bower_components/virtual-dom/dist/virtual-dom",
     "rbush": "../bower_components/rbush/rbush",
-    "helper": "../helper",
-    "jshashes": "../bower_components/jshashes/hashes"
+    "helper": "../helper"
   },
   shim: {
     "leaflet.label": ["leaflet"],
diff --git a/bower.json b/bower.json
index ef1fd19..69d1b13 100644
--- a/bower.json
+++ b/bower.json
@@ -21,8 +21,7 @@
     "roboto-fontface": "~0.3.0",
     "virtual-dom": "~2.1.1",
     "leaflet-providers": "~1.1.10",
-    "rbush": "https://github.com/mourner/rbush.git#~1.4.3",
-    "jshashes": "~1.0.5"
+    "rbush": "https://github.com/mourner/rbush.git#~1.4.3"
   },
   "authors": [
     "Milan Pässler <me@petabyteboy.de>",
diff --git a/lib/map/clientlayer.js b/lib/map/clientlayer.js
index d6ee4fc..7579f4e 100644
--- a/lib/map/clientlayer.js
+++ b/lib/map/clientlayer.js
@@ -1,6 +1,5 @@
-define(["leaflet", "jshashes"],
-  function (L, jsHashes) {
-    var MD5 = new jsHashes.MD5();
+define(["leaflet"],
+  function (L) {
 
     return L.TileLayer.Canvas.extend({
       setData: function (d) {
@@ -8,8 +7,7 @@ define(["leaflet", "jshashes"],
 
         //pre-calculate start angles
         this.data.all().forEach(function (d) {
-          var hash = MD5.hex(d.node.nodeinfo.node_id);
-          d.startAngle = (parseInt(hash.substr(0, 2), 16) / 255) * 2 * Math.PI;
+          d.startAngle = (parseInt(d.node.nodeinfo.node_id.substr(10, 2), 16) / 255) * 2 * Math.PI;
         });
         this.redraw();
       },

From c4517750213df395f3efb740d21b26092d27703c Mon Sep 17 00:00:00 2001
From: Xaver Maierhofer <xaver.maierhofer@xwissen.info>
Date: Thu, 26 May 2016 01:48:56 +0200
Subject: [PATCH 24/32] [TASK] Remove tablesort number No complex different
 styled numbers need to be sorted

---
 app.js              | 6 ++----
 lib/infobox/node.js | 2 +-
 2 files changed, 3 insertions(+), 5 deletions(-)

diff --git a/app.js b/app.js
index 8720290..899049a 100644
--- a/app.js
+++ b/app.js
@@ -7,8 +7,7 @@ require.config({
     "chroma-js": "../bower_components/chroma-js/chroma.min",
     "moment": "../bower_components/moment/min/moment.min",
     "moment.de": "../bower_components/moment/locale/de",
-    "tablesort": "../bower_components/tablesort/tablesort.min",
-    "tablesort.number": "../bower_components/tablesort/src/sorts/tablesort.number",
+    "tablesort": "../bower_components/tablesort/src/tablesort",
     "d3": "../bower_components/d3/d3.min",
     "virtual-dom": "../bower_components/virtual-dom/dist/virtual-dom",
     "rbush": "../bower_components/rbush/rbush",
@@ -20,8 +19,7 @@ require.config({
     "moment.de": ["moment"],
     "tablesort": {
       exports: "Tablesort"
-    },
-    "tablesort.number": ["tablesort"]
+    }
   }
 });
 
diff --git a/lib/infobox/node.js b/lib/infobox/node.js
index 421a60d..415596b 100644
--- a/lib/infobox/node.js
+++ b/lib/infobox/node.js
@@ -1,4 +1,4 @@
-define(["moment", "tablesort", "tablesort.number", "moment.de"],
+define(["moment", "tablesort", "moment.de"],
   function (moment, Tablesort) {
     function showGeoURI(d) {
       function showLatitude(d) {

From 173674c2a16d2964c9a48f6931a20d0c55d6777b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Milan=20P=C3=A4ssler?= <me@petabyteboy.de>
Date: Sat, 18 Mar 2017 15:33:49 +0100
Subject: [PATCH 25/32] [!!!][TASK] Use eslint default

---
 .eslintrc                  |   8 ++
 .gitignore                 |   1 -
 app.js                     |   6 +-
 lib/filters/genericnode.js |   4 +-
 lib/forcegraph.js          |   6 +-
 lib/infobox/link.js        |  18 +--
 lib/infobox/location.js    |   6 +-
 lib/infobox/main.js        |   6 +-
 lib/infobox/node.js        | 106 ++++++++--------
 lib/linklist.js            |   6 +-
 lib/main.js                |  17 +--
 lib/map.js                 |  36 +++---
 lib/meshstats.js           |  18 +--
 lib/nodelist.js            |   4 +-
 lib/proportions.js         |  20 +--
 lib/router.js              |   4 +-
 lib/simplenodelist.js      |   4 +-
 lib/utils/helper.js        | 241 +++++++++++++++++++++++++++++++++++++
 package.json               |  20 +--
 tasks/development.js       |   6 +-
 tasks/linting.js           |   9 --
 21 files changed, 387 insertions(+), 159 deletions(-)
 create mode 100644 .eslintrc
 create mode 100644 lib/utils/helper.js

diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..f0764d2
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,8 @@
+---
+"extends":
+  - "defaults/configurations/eslint"
+
+rules:
+  "semi": ["error", "always"]
+  "no-undef": 0
+  "no-console": ["error", { allow: ["warn", "error"] }]
diff --git a/.gitignore b/.gitignore
index e728065..d7fc55a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,3 @@ build/
 .sass-cache/
 config.json
 .idea/
-.eslintrc
diff --git a/app.js b/app.js
index 899049a..d6a1227 100644
--- a/app.js
+++ b/app.js
@@ -11,7 +11,7 @@ require.config({
     "d3": "../bower_components/d3/d3.min",
     "virtual-dom": "../bower_components/virtual-dom/dist/virtual-dom",
     "rbush": "../bower_components/rbush/rbush",
-    "helper": "../helper"
+    "helper": "utils/helper"
   },
   shim: {
     "leaflet.label": ["leaflet"],
@@ -23,6 +23,6 @@ require.config({
   }
 });
 
-require(["main", "helper"], function (main) {
-  getJSON("config.json").then(main);
+require(["main", "helper"], function (main, helper) {
+  helper.getJSON("config.json").then(main);
 });
diff --git a/lib/filters/genericnode.js b/lib/filters/genericnode.js
index 831f575..c4fe7a9 100644
--- a/lib/filters/genericnode.js
+++ b/lib/filters/genericnode.js
@@ -1,4 +1,4 @@
-define([], function () {
+define(["helper"], function (helper) {
   return function (name, key, value, f) {
     var negate = false;
     var refresh;
@@ -9,7 +9,7 @@ define([], function () {
     label.appendChild(strong);
 
     function run(d) {
-      var o = dictGet(d, key.slice(0));
+      var o = helper.dictGet(d, key.slice(0));
 
       if (f) {
         o = f(o);
diff --git a/lib/forcegraph.js b/lib/forcegraph.js
index d1a24d3..645b053 100644
--- a/lib/forcegraph.js
+++ b/lib/forcegraph.js
@@ -1,4 +1,4 @@
-define(["d3"], function (d3) {
+define(["d3", "helper"], function (d3, helper) {
   var margin = 200;
   var NODE_RADIUS = 15;
   var LINE_RADIUS = 7;
@@ -32,7 +32,7 @@ define(["d3"], function (d3) {
     }
 
     function savePositions() {
-      if (!localStorageTest()) {
+      if (!helper.localStorageTest()) {
         return;
       }
 
@@ -773,7 +773,7 @@ define(["d3"], function (d3) {
         return !d.o.node;
       });
 
-      if (localStorageTest()) {
+      if (helper.localStorageTest()) {
         var save = JSON.parse(localStorage.getItem("graph/nodeposition"));
 
         if (save) {
diff --git a/lib/infobox/link.js b/lib/infobox/link.js
index 260931f..d24e144 100644
--- a/lib/infobox/link.js
+++ b/lib/infobox/link.js
@@ -1,11 +1,11 @@
-define(function () {
+define(["helper"], function (helper) {
   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 helper.showStat(o, subst);
   }
 
   return function (config, el, router, d) {
@@ -19,7 +19,7 @@ define(function () {
     a1.textContent = unknown ? d.source.id : d.source.node.nodeinfo.hostname;
     h2.appendChild(a1);
     h2.appendChild(document.createTextNode(" \uF3D6 "));
-    h2.className = 'ion-inside';
+    h2.className = "ion-inside";
     var a2 = document.createElement("a");
     a2.href = "#";
     a2.onclick = router.node(d.target.node);
@@ -30,12 +30,12 @@ define(function () {
     var attributes = document.createElement("table");
     attributes.classList.add("attributes");
 
-    attributeEntry(attributes, "TQ", showTq(d));
-    attributeEntry(attributes, "Entfernung", showDistance(d));
-    attributeEntry(attributes, "Typ", d.type);
-    var hw1 = unknown ? null : dictGet(d.source.node.nodeinfo, ["hardware", "model"]);
-    var hw2 = dictGet(d.target.node.nodeinfo, ["hardware", "model"]);
-    attributeEntry(attributes, "Hardware", (hw1 != null ? hw1 : "unbekannt") + " – " + (hw2 != null ? hw2 : "unbekannt"));
+    helper.attributeEntry(attributes, "TQ", helper.showTq(d));
+    helper.attributeEntry(attributes, "Entfernung", helper.showDistance(d));
+    helper.attributeEntry(attributes, "Typ", d.type);
+    var hw1 = unknown ? null : helper.dictGet(d.source.node.nodeinfo, ["hardware", "model"]);
+    var hw2 = helper.dictGet(d.target.node.nodeinfo, ["hardware", "model"]);
+    helper.attributeEntry(attributes, "Hardware", (hw1 != null ? hw1 : "unbekannt") + " – " + (hw2 != null ? hw2 : "unbekannt"));
     el.appendChild(attributes);
 
     if (config.linkInfos) {
diff --git a/lib/infobox/location.js b/lib/infobox/location.js
index 9910d33..4695ac4 100644
--- a/lib/infobox/location.js
+++ b/lib/infobox/location.js
@@ -1,10 +1,10 @@
-define(function () {
+define(["helper"], function (helper) {
   return function (config, el, router, d) {
     var sidebarTitle = document.createElement("h2");
     sidebarTitle.textContent = "Location: " + d.toString();
     el.appendChild(sidebarTitle);
 
-    getJSON("https://nominatim.openstreetmap.org/reverse?format=json&lat=" + d.lat + "&lon=" + d.lng + "&zoom=18&addressdetails=0")
+    helper.getJSON("https://nominatim.openstreetmap.org/reverse?format=json&lat=" + d.lat + "&lon=" + d.lng + "&zoom=18&addressdetails=0")
       .then(function (result) {
         if (result.display_name) {
           sidebarTitle.textContent = result.display_name;
@@ -84,7 +84,7 @@ define(function () {
       try {
         document.execCommand("copy");
       } catch (err) {
-        console.log(err);
+        console.warn(err);
       }
     }
 
diff --git a/lib/infobox/main.js b/lib/infobox/main.js
index dc900e4..8f1ef4a 100644
--- a/lib/infobox/main.js
+++ b/lib/infobox/main.js
@@ -33,17 +33,17 @@ define(["infobox/link", "infobox/node", "infobox/location"], function (Link, Nod
 
     self.gotoNode = function (d) {
       create();
-      new Node(config, el, router, d);
+      Node(config, el, router, d);
     };
 
     self.gotoLink = function (d) {
       create();
-      new Link(config, el, router, d);
+      Link(config, el, router, d);
     };
 
     self.gotoLocation = function (d) {
       create();
-      new Location(config, el, router, d);
+      Location(config, el, router, d);
     };
 
     return self;
diff --git a/lib/infobox/node.js b/lib/infobox/node.js
index 415596b..6839206 100644
--- a/lib/infobox/node.js
+++ b/lib/infobox/node.js
@@ -1,5 +1,5 @@
-define(["moment", "tablesort", "moment.de"],
-  function (moment, Tablesort) {
+define(["moment", "tablesort", "helper", "moment.de"],
+  function (moment, Tablesort, helper) {
     function showGeoURI(d) {
       function showLatitude(d) {
         var suffix = Math.sign(d) > -1 ? "' N" : "' S";
@@ -21,7 +21,7 @@ define(["moment", "tablesort", "moment.de"],
         return a + "° " + min.toFixed(3) + suffix;
       }
 
-      if (!has_location(d)) {
+      if (!helper.hasLocation(d)) {
         return undefined;
       }
 
@@ -49,8 +49,8 @@ define(["moment", "tablesort", "moment.de"],
     }
 
     function showFirmware(d) {
-      var release = dictGet(d.nodeinfo, ["software", "firmware", "release"]);
-      var base = dictGet(d.nodeinfo, ["software", "firmware", "base"]);
+      var release = helper.dictGet(d.nodeinfo, ["software", "firmware", "release"]);
+      var base = helper.dictGet(d.nodeinfo, ["software", "firmware", "base"]);
 
       if (release === null || base === null) {
         return undefined;
@@ -60,7 +60,7 @@ define(["moment", "tablesort", "moment.de"],
     }
 
     function showSite(d, config) {
-      var site = dictGet(d.nodeinfo, ["system", "site_code"]);
+      var site = helper.dictGet(d.nodeinfo, ["system", "site_code"]);
       var rt = site;
       if (config.siteNames) {
         config.siteNames.forEach(function (t) {
@@ -220,7 +220,7 @@ define(["moment", "tablesort", "moment.de"],
     }
 
     function showIPs(d) {
-      var ips = dictGet(d.nodeinfo, ["network", "addresses"]);
+      var ips = helper.dictGet(d.nodeinfo, ["network", "addresses"]);
       if (ips === null) {
         return undefined;
       }
@@ -340,13 +340,13 @@ define(["moment", "tablesort", "moment.de"],
 
     function showGateway(d, router) {
       var nh;
-      if (dictGet(d.statistics, ["nexthop"])) {
-        nh = dictGet(d.statistics, ["nexthop"]);
+      if (helper.dictGet(d.statistics, ["nexthop"])) {
+        nh = helper.dictGet(d.statistics, ["nexthop"]);
       }
-      if (dictGet(d.statistics, ["gateway_nexthop"])) {
-        nh = dictGet(d.statistics, ["gateway_nexthop"]);
+      if (helper.dictGet(d.statistics, ["gateway_nexthop"])) {
+        nh = helper.dictGet(d.statistics, ["gateway_nexthop"]);
       }
-      var gw = dictGet(d.statistics, ["gateway"]);
+      var gw = helper.dictGet(d.statistics, ["gateway"]);
 
       if (!gw) {
         return null;
@@ -362,16 +362,16 @@ define(["moment", "tablesort", "moment.de"],
           if (!nh.node || !nh.node.statistics) {
             break;
           }
-          if (!dictGet(nh.node.statistics, ["gateway"]) || !dictGet(nh.node.statistics, ["gateway"]).id) {
+          if (!helper.dictGet(nh.node.statistics, ["gateway"]) || !helper.dictGet(nh.node.statistics, ["gateway"]).id) {
             break;
           }
-          if (dictGet(nh.node.statistics, ["gateway"]).id !== gw.id) {
+          if (helper.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"]);
+          if (helper.dictGet(nh.node.statistics, ["gateway_nexthop"])) {
+            nh = helper.dictGet(nh.node.statistics, ["gateway_nexthop"]);
+          } else if (helper.dictGet(nh.node.statistics, ["nexthop"])) {
+            nh = helper.dictGet(nh.node.statistics, ["nexthop"]);
           } else {
             break;
           }
@@ -391,7 +391,7 @@ define(["moment", "tablesort", "moment.de"],
     }
 
     function showPages(d) {
-      var webpages = dictGet(d.nodeinfo, ["pages"]);
+      var webpages = helper.dictGet(d.nodeinfo, ["pages"]);
       if (webpages === null) {
         return undefined;
       }
@@ -425,7 +425,7 @@ define(["moment", "tablesort", "moment.de"],
     }
 
     function showAutoupdate(d) {
-      var au = dictGet(d.nodeinfo, ["software", "autoupdater"]);
+      var au = helper.dictGet(d.nodeinfo, ["software", "autoupdater"]);
       if (!au) {
         return undefined;
       }
@@ -469,8 +469,8 @@ define(["moment", "tablesort", "moment.de"],
     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.replace(/[^a-z0-9\-]/ig, '_') : "unknown";
-      return showStat(o, subst);
+      subst["{NODE_NAME}"] = d.nodeinfo.hostname ? d.nodeinfo.hostname.replace(/[^a-z0-9\-]/ig, "_") : "unknown";
+      return helper.showStat(o, subst);
     }
 
     return function (config, el, router, d) {
@@ -483,47 +483,47 @@ define(["moment", "tablesort", "moment.de"],
         try {
           config.hwImg.forEach(function (hwImg) {
             try {
-              top.appendChild(showNodeImg(hwImg, dictGet(d, ["nodeinfo", "hardware", "model"])));
+              top.appendChild(showNodeImg(hwImg, helper.dictGet(d, ["nodeinfo", "hardware", "model"])));
             } catch (err) {
-              console.log(err.message);
+              console.warn(err.message);
             }
           });
         } catch (err) {
-          console.log(err.message);
+          console.warn(err.message);
         }
-        attributeEntry(attributes, top, d.nodeinfo.hostname);
+        helper.attributeEntry(attributes, top, d.nodeinfo.hostname);
       } else {
         var h2 = document.createElement("h2");
         h2.textContent = d.nodeinfo.hostname;
         el.appendChild(h2);
       }
 
-      attributeEntry(attributes, "Status", showStatus(d));
-      attributeEntry(attributes, "Gateway", d.flags.gateway ? "ja" : null);
-      attributeEntry(attributes, "Koordinaten", showGeoURI(d));
+      helper.attributeEntry(attributes, "Status", showStatus(d));
+      helper.attributeEntry(attributes, "Gateway", d.flags.gateway ? "ja" : null);
+      helper.attributeEntry(attributes, "Koordinaten", showGeoURI(d));
 
       if (config.showContact) {
-        attributeEntry(attributes, "Kontakt", dictGet(d.nodeinfo, ["owner", "contact"]));
+        helper.attributeEntry(attributes, "Kontakt", helper.dictGet(d.nodeinfo, ["owner", "contact"]));
       }
 
-      attributeEntry(attributes, "Hardware", dictGet(d.nodeinfo, ["hardware", "model"]));
-      attributeEntry(attributes, "Primäre MAC", dictGet(d.nodeinfo, ["network", "mac"]));
-      attributeEntry(attributes, "Node ID", dictGet(d.nodeinfo, ["node_id"]));
-      attributeEntry(attributes, "Firmware", showFirmware(d));
-      attributeEntry(attributes, "Site", showSite(d, config));
-      attributeEntry(attributes, "Uptime", showUptime(d));
-      attributeEntry(attributes, "Teil des Netzes", showFirstseen(d));
-      attributeEntry(attributes, "Kanal 2.4 GHz", showWifiChannel(dictGet(d.nodeinfo, ["wireless", "chan2"])));
-      attributeEntry(attributes, "Kanal 5 GHz", showWifiChannel(dictGet(d.nodeinfo, ["wireless", "chan5"])));
-      attributeEntry(attributes, "Airtime 2.4 GHz", showAirtime(2, dictGet(d.statistics, ["wireless", "airtime2"])));
-      attributeEntry(attributes, "Airtime 5 GHz", showAirtime(5, dictGet(d.statistics, ["wireless", "airtime5"])));
-      attributeEntry(attributes, "Systemlast", showLoad(d));
-      attributeEntry(attributes, "Arbeitsspeicher", showRAM(d));
-      attributeEntry(attributes, "IP Adressen", showIPs(d));
-      attributeEntry(attributes, "Webseite", showPages(d));
-      attributeEntry(attributes, "Gewähltes Gateway", showGateway(d, router));
-      attributeEntry(attributes, "Autom. Updates", showAutoupdate(d));
-      attributeEntry(attributes, "Clients", showClients(d), showMeshClients(d));
+      helper.attributeEntry(attributes, "Hardware", helper.dictGet(d.nodeinfo, ["hardware", "model"]));
+      helper.attributeEntry(attributes, "Primäre MAC", helper.dictGet(d.nodeinfo, ["network", "mac"]));
+      helper.attributeEntry(attributes, "Node ID", helper.dictGet(d.nodeinfo, ["node_id"]));
+      helper.attributeEntry(attributes, "Firmware", showFirmware(d));
+      helper.attributeEntry(attributes, "Site", showSite(d, config));
+      helper.attributeEntry(attributes, "Uptime", showUptime(d));
+      helper.attributeEntry(attributes, "Teil des Netzes", showFirstseen(d));
+      helper.attributeEntry(attributes, "Kanal 2.4 GHz", showWifiChannel(helper.dictGet(d.nodeinfo, ["wireless", "chan2"])));
+      helper.attributeEntry(attributes, "Kanal 5 GHz", showWifiChannel(helper.dictGet(d.nodeinfo, ["wireless", "chan5"])));
+      helper.attributeEntry(attributes, "Airtime 2.4 GHz", showAirtime(2, helper.dictGet(d.statistics, ["wireless", "airtime2"])));
+      helper.attributeEntry(attributes, "Airtime 5 GHz", showAirtime(5, helper.dictGet(d.statistics, ["wireless", "airtime5"])));
+      helper.attributeEntry(attributes, "Systemlast", showLoad(d));
+      helper.attributeEntry(attributes, "Arbeitsspeicher", showRAM(d));
+      helper.attributeEntry(attributes, "IP Adressen", showIPs(d));
+      helper.attributeEntry(attributes, "Webseite", showPages(d));
+      helper.attributeEntry(attributes, "Gewähltes Gateway", showGateway(d, router));
+      helper.attributeEntry(attributes, "Autom. Updates", showAutoupdate(d));
+      helper.attributeEntry(attributes, "Clients", showClients(d), showMeshClients(d));
 
       el.appendChild(attributes);
 
@@ -577,14 +577,14 @@ define(["moment", "tablesort", "moment.de"],
           var tr = document.createElement("tr");
 
           var td1 = document.createElement("td");
-          td1.className = 'ion-inside';
+          td1.className = "ion-inside";
           td1.appendChild(document.createTextNode(d.incoming ? " \uF3D5 " : " \uF3D6 "));
           tr.appendChild(td1);
 
           var td2 = document.createElement("td");
           td2.appendChild(createLink(d, router));
 
-          if (!unknown && has_location(d.node)) {
+          if (!unknown && helper.hasLocation(d.node)) {
             var span = document.createElement("span");
             span.classList.add("icon");
             span.classList.add("ion-location");
@@ -596,7 +596,7 @@ define(["moment", "tablesort", "moment.de"],
           var td3 = document.createElement("td");
           var a2 = document.createElement("a");
           a2.href = "#";
-          a2.textContent = showTq(d.link);
+          a2.textContent = helper.showTq(d.link);
           a2.onclick = router.link(d.link);
           td3.appendChild(a2);
           tr.appendChild(td3);
@@ -612,7 +612,7 @@ define(["moment", "tablesort", "moment.de"],
           var td5 = document.createElement("td");
           var a4 = document.createElement("a");
           a4.href = "#";
-          a4.textContent = showDistance(d.link);
+          a4.textContent = helper.showDistance(d.link);
           a4.onclick = router.link(d.link);
           td5.appendChild(a4);
           td5.setAttribute("data-sort", d.link.distance !== undefined ? -d.link.distance : 1);
@@ -624,7 +624,7 @@ define(["moment", "tablesort", "moment.de"],
         table.appendChild(tbody);
         table.className = "node-links";
 
-        new Tablesort(table);
+        Tablesort(table);
 
         el.appendChild(table);
       }
diff --git a/lib/linklist.js b/lib/linklist.js
index 96e7447..661b777 100644
--- a/lib/linklist.js
+++ b/lib/linklist.js
@@ -1,4 +1,4 @@
-define(["sorttable", "virtual-dom"], function (SortTable, V) {
+define(["sorttable", "virtual-dom", "helper"], function (SortTable, V, helper) {
   function linkName(d) {
     return (d.source.node ? d.source.node.nodeinfo.hostname : d.source.id) + " – " + d.target.node.nodeinfo.hostname;
   }
@@ -40,9 +40,9 @@ define(["sorttable", "virtual-dom"], function (SortTable, V) {
       var td1Content = [V.h("a", {href: "#", onclick: router.link(d)}, linkName(d))];
 
       var td1 = V.h("td", td1Content);
-      var td2 = V.h("td", {style: {color: linkScale(d.tq).hex()}}, showTq(d));
+      var td2 = V.h("td", {style: {color: linkScale(d.tq).hex()}}, helper.showTq(d));
       var td3 = V.h("td", d.type);
-      var td4 = V.h("td", showDistance(d));
+      var td4 = V.h("td", helper.showDistance(d));
 
       return V.h("tr", [td1, td2, td3, td4]);
     }
diff --git a/lib/main.js b/lib/main.js
index aa4163a..36d421c 100644
--- a/lib/main.js
+++ b/lib/main.js
@@ -1,5 +1,5 @@
-define(["moment", "router", "leaflet", "gui", "moment.de"],
-  function (moment, Router, L, GUI) {
+define(["moment", "router", "leaflet", "gui", "helper", "moment.de"],
+  function (moment, Router, L, GUI, helper) {
     return function (config) {
       function handleData(data) {
         var dataNodes = {};
@@ -21,7 +21,7 @@ define(["moment", "router", "leaflet", "gui", "moment.de"],
           if (i % 2) {
             if (data[i].version !== 1) {
               vererr = "Unsupported graph version: " + data[i].version;
-              console.log(vererr); //silent fail
+              console.error(vererr); //silent fail
             } else {
               data[i].batadv.links.forEach(rearrangeLinks);
               dataGraph.batadv.nodes = dataGraph.batadv.nodes.concat(data[i].batadv.nodes);
@@ -30,7 +30,7 @@ define(["moment", "router", "leaflet", "gui", "moment.de"],
             }
           } else if (data[i].version !== 2) {
             vererr = "Unsupported nodes version: " + data[i].version;
-            console.log(vererr); //silent fail
+            console.error(vererr); //silent fail
           } else {
             data[i].nodes.forEach(fillData);
             dataNodes.timestamp = data[i].timestamp;
@@ -60,8 +60,8 @@ define(["moment", "router", "leaflet", "gui", "moment.de"],
         var now = moment();
         var age = moment(now).subtract(config.maxAge, "days");
 
-        var newnodes = limit("firstseen", age, sortByKey("firstseen", nodes).filter(online));
-        var lostnodes = limit("lastseen", age, sortByKey("lastseen", nodes).filter(offline));
+        var newnodes = helper.limit("firstseen", age, helper.sortByKey("firstseen", nodes).filter(helper.online));
+        var lostnodes = helper.limit("lastseen", age, helper.sortByKey("lastseen", nodes).filter(helper.offline));
 
         var graphnodes = {};
 
@@ -197,6 +197,7 @@ define(["moment", "router", "leaflet", "gui", "moment.de"],
           }
         };
       }
+
       moment.locale("de");
 
       var router = new Router();
@@ -213,7 +214,7 @@ define(["moment", "router", "leaflet", "gui", "moment.de"],
       }
 
       function update() {
-        return Promise.all(urls.map(getJSON))
+        return Promise.all(urls.map(helper.getJSON))
           .then(handleData);
       }
 
@@ -233,7 +234,7 @@ define(["moment", "router", "leaflet", "gui", "moment.de"],
         })
         .catch(function (e) {
           document.body.textContent = e;
-          console.log(e);
+          console.warn(e);
         });
     };
   });
diff --git a/lib/map.js b/lib/map.js
index 7269023..f099fb6 100644
--- a/lib/map.js
+++ b/lib/map.js
@@ -1,7 +1,7 @@
 define(["map/clientlayer", "map/labelslayer",
-    "d3", "leaflet", "moment", "locationmarker", "rbush",
+    "d3", "leaflet", "moment", "locationmarker", "rbush", "helper",
     "leaflet.label", "leaflet.providers", "moment.de"],
-  function (ClientLayer, LabelsLayer, d3, L, moment, LocationMarker, rbush) {
+  function (ClientLayer, LabelsLayer, d3, L, moment, LocationMarker, rbush, helper) {
     var options = {
       worldCopyJump: true,
       zoomControl: false
@@ -149,7 +149,7 @@ define(["map/clientlayer", "map/labelslayer",
           line.setStyle(opts);
         };
 
-        line.bindLabel(d.source.node.nodeinfo.hostname + " – " + d.target.node.nodeinfo.hostname + "<br><strong>" + showDistance(d) + " / " + showTq(d) + "</strong>");
+        line.bindLabel(d.source.node.nodeinfo.hostname + " – " + d.target.node.nodeinfo.hostname + "<br><strong>" + helper.showDistance(d) + " / " + helper.showTq(d) + "</strong>");
         line.on("click", router.link(d));
 
         dict[d.id] = line;
@@ -307,7 +307,7 @@ define(["map/clientlayer", "map/labelslayer",
           layerControl.addBaseLayer(layer, layerName);
           customLayers[layerName] = layer;
 
-          if (localStorageTest()) {
+          if (helper.localStorageTest()) {
             localStorage.setItem("map/customLayers", JSON.stringify(Object.keys(customLayers)));
           }
         } catch (e) {
@@ -354,7 +354,7 @@ define(["map/clientlayer", "map/labelslayer",
       layerControl = L.control.layers(baseLayers, [], {position: "bottomright"});
       layerControl.addTo(map);
 
-      if (localStorageTest()) {
+      if (helper.localStorageTest()) {
         var d = JSON.parse(localStorage.getItem("map/customLayers"));
 
         if (d) {
@@ -385,7 +385,7 @@ define(["map/clientlayer", "map/labelslayer",
         if (map.getZoom() > map.options.maxZoom) {
           map.setZoom(map.options.maxZoom);
         }
-        if (localStorageTest()) {
+        if (helper.localStorageTest()) {
           localStorage.setItem("map/selectedLayer", JSON.stringify({name: e.name}));
         }
       });
@@ -531,30 +531,30 @@ define(["map/clientlayer", "map/labelslayer",
         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(helper.hasLocation));
         } 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 nodesOffline = subtract(data.nodes.all.filter(offline), data.nodes.lost);
+        var nodesOnline = helper.subtract(data.nodes.all.filter(helper.online), data.nodes.new);
+        var nodesOffline = helper.subtract(data.nodes.all.filter(helper.offline), data.nodes.lost);
 
-        var markersOnline = nodesOnline.filter(has_location)
+        var markersOnline = nodesOnline.filter(helper.hasLocation)
           .map(mkMarker(nodeDict, function () {
             return iconOnline;
           }, router));
 
-        var markersOffline = nodesOffline.filter(has_location)
+        var markersOffline = nodesOffline.filter(helper.hasLocation)
           .map(mkMarker(nodeDict, function () {
             return iconOffline;
           }, router));
 
-        var markersNew = data.nodes.new.filter(has_location)
+        var markersNew = data.nodes.new.filter(helper.hasLocation)
           .map(mkMarker(nodeDict, function () {
             return iconNew;
           }, router));
 
-        var markersLost = data.nodes.lost.filter(has_location)
+        var markersLost = data.nodes.lost.filter(helper.hasLocation)
           .map(mkMarker(nodeDict, function (d) {
             if (d.lastseen.isAfter(moment(data.now).subtract(3, "days"))) {
               return iconAlert;
@@ -572,14 +572,14 @@ define(["map/clientlayer", "map/labelslayer",
 
         var rtreeOnlineAll = rbush(9);
 
-        rtreeOnlineAll.load(data.nodes.all.filter(online).filter(has_location).map(mapRTree));
+        rtreeOnlineAll.load(data.nodes.all.filter(helper.online).filter(helper.hasLocation).map(mapRTree));
 
         clientLayer.setData(rtreeOnlineAll);
         labelsLayer.setData({
-          online: nodesOnline.filter(has_location),
-          offline: nodesOffline.filter(has_location),
-          new: data.nodes.new.filter(has_location),
-          lost: data.nodes.lost.filter(has_location)
+          online: nodesOnline.filter(helper.hasLocation),
+          offline: nodesOffline.filter(helper.hasLocation),
+          new: data.nodes.new.filter(helper.hasLocation),
+          lost: data.nodes.lost.filter(helper.hasLocation)
         });
 
         updateView(true);
diff --git a/lib/meshstats.js b/lib/meshstats.js
index 0abd3cc..bc6c025 100644
--- a/lib/meshstats.js
+++ b/lib/meshstats.js
@@ -1,20 +1,20 @@
-define(function () {
+define(["helper"], function (helper) {
   return function (config) {
     var self = this;
     var stats, timestamp;
 
     self.setData = function (d) {
-      var totalNodes = sum(d.nodes.all.map(one));
-      var totalOnlineNodes = sum(d.nodes.all.filter(online).map(one));
-      var totalOfflineNodes = sum(d.nodes.all.filter(function (node) {
+      var totalNodes = helper.sum(d.nodes.all.map(helper.one));
+      var totalOnlineNodes = helper.sum(d.nodes.all.filter(helper.online).map(helper.one));
+      var totalOfflineNodes = helper.sum(d.nodes.all.filter(function (node) {
         return !node.flags.online;
-      }).map(one));
-      var totalNewNodes = sum(d.nodes.new.map(one));
-      var totalLostNodes = sum(d.nodes.lost.map(one));
-      var totalClients = sum(d.nodes.all.filter(online).map(function (d) {
+      }).map(helper.one));
+      var totalNewNodes = helper.sum(d.nodes.new.map(helper.one));
+      var totalLostNodes = helper.sum(d.nodes.lost.map(helper.one));
+      var totalClients = helper.sum(d.nodes.all.filter(helper.online).map(function (d) {
         return d.statistics.clients ? d.statistics.clients : 0;
       }));
-      var totalGateways = sum(Array.from(new Set(d.nodes.all.filter(online).map(function (d) {
+      var totalGateways = helper.sum(Array.from(new Set(d.nodes.all.filter(helper.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;
diff --git a/lib/nodelist.js b/lib/nodelist.js
index 950bdd3..12687c4 100644
--- a/lib/nodelist.js
+++ b/lib/nodelist.js
@@ -1,4 +1,4 @@
-define(["sorttable", "virtual-dom"], function (SortTable, V) {
+define(["sorttable", "virtual-dom", "helper"], function (SortTable, V, helper) {
   function getUptime(now, d) {
     if (d.flags.online && "uptime" in d.statistics) {
       return Math.round(d.statistics.uptime);
@@ -68,7 +68,7 @@ define(["sorttable", "virtual-dom"], function (SortTable, V) {
         href: "#!n:" + d.nodeinfo.node_id
       }, d.nodeinfo.hostname));
 
-      if (has_location(d)) {
+      if (helper.hasLocation(d)) {
         td1Content.push(V.h("span", {className: "icon ion-location"}));
       }
 
diff --git a/lib/proportions.js b/lib/proportions.js
index 6bf6390..391145e 100644
--- a/lib/proportions.js
+++ b/lib/proportions.js
@@ -1,5 +1,5 @@
-define(["chroma-js", "virtual-dom", "filters/genericnode"],
-  function (Chroma, V, Filter) {
+define(["chroma-js", "virtual-dom", "filters/genericnode", "helper"],
+  function (Chroma, V, Filter, helper) {
 
     return function (config, filterManager) {
       var self = this;
@@ -33,14 +33,14 @@ define(["chroma-js", "virtual-dom", "filters/genericnode"],
       siteTable.classList.add("proportion");
 
       function showStatGlobal(o) {
-        return showStat(o);
+        return helper.showStat(o);
       }
 
       function count(nodes, key, f) {
         var dict = {};
 
         nodes.forEach(function (d) {
-          var v = dictGet(d, key.slice(0));
+          var v = helper.dictGet(d, key.slice(0));
 
           if (f !== undefined) {
             v = f(v);
@@ -62,7 +62,7 @@ define(["chroma-js", "virtual-dom", "filters/genericnode"],
         var dict = {};
 
         nodes.forEach(function (d) {
-          var v = dictGet(d, key.slice(0));
+          var v = helper.dictGet(d, key.slice(0));
 
           if (f !== undefined) {
             v = f(v);
@@ -127,7 +127,7 @@ define(["chroma-js", "virtual-dom", "filters/genericnode"],
       }
 
       self.setData = function (data) {
-        var onlineNodes = data.nodes.all.filter(online);
+        var onlineNodes = data.nodes.all.filter(helper.online);
         var nodes = onlineNodes.concat(data.nodes.lost);
         var nodeDict = {};
 
@@ -214,8 +214,12 @@ define(["chroma-js", "virtual-dom", "filters/genericnode"],
           return b[1] - a[1];
         }));
         fillTable("Firmware", fwTable, fwDict.sort(function (a, b) {
-          if(b[0] < a[0]) return -1;
-          if(b[0] > a[0]) return 1;
+          if (b[0] < a[0]) {
+            return -1;
+          }
+          if (b[0] > a[0]) {
+            return 1;
+          }
           return 0;
         }));
         fillTable("Hardware", hwTable, hwDict.sort(function (a, b) {
diff --git a/lib/router.js b/lib/router.js
index f719cc0..d841d32 100644
--- a/lib/router.js
+++ b/lib/router.js
@@ -1,4 +1,4 @@
-define(function () {
+define(["helper"], function (helper) {
   return function () {
     var self = this;
     var objects = {nodes: {}, links: {}};
@@ -31,7 +31,7 @@ define(function () {
     }
 
     function resetView(push) {
-      push = trueDefault(push);
+      push = helper.trueDefault(push);
 
       targets.forEach(function (t) {
         t.resetView();
diff --git a/lib/simplenodelist.js b/lib/simplenodelist.js
index bac6c25..2ba0207 100644
--- a/lib/simplenodelist.js
+++ b/lib/simplenodelist.js
@@ -1,4 +1,4 @@
-define(["moment", "virtual-dom", "moment.de"], function (moment, V) {
+define(["moment", "virtual-dom", "helper", "moment.de"], function (moment, V, helper) {
   return function (nodes, field, router, title) {
     var self = this;
     var el, tbody;
@@ -46,7 +46,7 @@ define(["moment", "virtual-dom", "moment.de"], function (moment, V) {
           href: "#!n:" + d.nodeinfo.node_id
         }, d.nodeinfo.hostname));
 
-        if (has_location(d)) {
+        if (helper.hasLocation(d)) {
           td1Content.push(V.h("span", {className: "icon ion-location"}));
         }
 
diff --git a/lib/utils/helper.js b/lib/utils/helper.js
new file mode 100644
index 0000000..e3dce69
--- /dev/null
+++ b/lib/utils/helper.js
@@ -0,0 +1,241 @@
+define({
+  get: function (url) {
+    return new Promise(function (resolve, reject) {
+      var req = new XMLHttpRequest();
+      req.open("GET", url);
+
+      req.onload = function () {
+        if (req.status == 200) {
+          resolve(req.response);
+        }
+        else {
+          reject(Error(req.statusText));
+        }
+      };
+
+      req.onerror = function () {
+        reject(Error("Network Error"));
+      };
+
+      req.send();
+    });
+  },
+
+  getJSON: function (url) {
+    return require("helper").get(url).then(JSON.parse);
+  },
+
+  sortByKey: function (key, d) {
+    return d.slice().sort(function (a, b) {
+      return a[key] - b[key];
+    }).reverse();
+  },
+
+  limit: function (key, m, d) {
+    return d.filter(function (d) {
+      return d[key].isAfter(m);
+    });
+  },
+
+  sum: function (a) {
+    return a.reduce(function (a, b) {
+      return a + b;
+    }, 0);
+  },
+
+  one: function () {
+    return 1;
+  },
+
+  trueDefault: function (d) {
+    return d === undefined ? true : d;
+  },
+
+  dictGet: function (dict, key) {
+    var k = key.shift();
+
+    if (!(k in dict)) {
+      return null;
+    }
+
+    if (key.length == 0) {
+      return dict[k];
+    }
+
+    return this.dictGet(dict[k], key);
+  },
+
+  localStorageTest: function () {
+    var test = "test";
+    try {
+      localStorage.setItem(test, test);
+      localStorage.removeItem(test);
+      return true;
+    } catch (e) {
+      return false;
+    }
+  },
+
+  listReplace: function (s, subst) {
+    for (key in subst) {
+      var re = new RegExp(key, "g");
+      s = s.replace(re, subst[key]);
+    }
+    return s;
+  },
+
+  /* Helpers working with nodes */
+
+  offline: function (d) {
+    return !d.flags.online;
+  },
+
+  online: function (d) {
+    return d.flags.online;
+  },
+
+  hasLocation: function (d) {
+    return "location" in d.nodeinfo &&
+      Math.abs(d.nodeinfo.location.latitude) < 90 &&
+      Math.abs(d.nodeinfo.location.longitude) < 180;
+  },
+
+  subtract: function (a, b) {
+    var ids = {};
+
+    b.forEach(function (d) {
+      ids[d.nodeinfo.node_id] = true;
+    });
+
+    return a.filter(function (d) {
+      return !(d.nodeinfo.node_id in ids);
+    });
+  },
+
+  /* Helpers working with links */
+
+  showDistance: function (d) {
+    if (isNaN(d.distance)) {
+      return;
+    }
+
+    return d.distance.toFixed(0) + " m";
+  },
+
+  showTq: function (d) {
+    return (1 / d.tq * 100).toFixed(0) + "%";
+  },
+
+  attributeEntry: function (el, label, value) {
+    if (value === null || value == undefined) {
+      return;
+    }
+
+    var tr = document.createElement("tr");
+    var th = document.createElement("th");
+
+    if (typeof label === "string") {
+      th.textContent = label;
+    } else {
+      th.appendChild(label);
+      tr.classList.add("routerpic");
+    }
+
+    tr.appendChild(th);
+
+    var td = document.createElement("td");
+
+    if (typeof value == "function") {
+      value(td);
+    } else {
+      td.appendChild(document.createTextNode(value));
+    }
+
+    tr.appendChild(td);
+
+    el.appendChild(tr);
+
+    return td;
+  },
+
+  createIframe: function (opt, width, height) {
+    var el = document.createElement("iframe");
+    width = typeof width !== "undefined" ? width : "525px";
+    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;
+  },
+
+  showStat: function (o, subst) {
+    var content, caption;
+    subst = typeof subst !== "undefined" ? subst : {};
+
+    if (o.thumbnail) {
+      content = document.createElement("img");
+      content.src = require("helper").listReplace(o.thumbnail, subst);
+    }
+
+    if (o.caption) {
+      caption = require("helper").listReplace(o.caption, subst);
+
+      if (!content) {
+        content = document.createTextNode(caption);
+      }
+    }
+
+    if (o.iframe) {
+      content = require("helper").createIframe(o.iframe, o.width, o.height);
+      if (o.iframe.src) {
+        content.src = require("helper").listReplace(o.iframe.src, subst);
+      } else {
+        content.src = require("helper").listReplace(o.iframe, subst);
+      }
+    }
+
+    var p = document.createElement("p");
+
+    if (o.href) {
+      var link = document.createElement("a");
+      link.target = "_blank";
+      link.href = require("helper").listReplace(o.href, subst);
+      link.appendChild(content);
+
+      if (caption && o.thumbnail) {
+        link.title = caption;
+      }
+
+      p.appendChild(link);
+    } else {
+      p.appendChild(content);
+    }
+
+    return p;
+  }
+});
diff --git a/package.json b/package.json
index 613c552..caa6d5c 100644
--- a/package.json
+++ b/package.json
@@ -5,6 +5,8 @@
   },
   "devDependencies": {
     "autoprefixer": "^6.3.6",
+    "eslint": "^2.10.2",
+    "eslint-config-defaults": "^9.0.0",
     "grunt": "^1.0.1",
     "grunt-bower-install-simple": "^1.2.3",
     "grunt-check-dependencies": "^0.12.0",
@@ -26,24 +28,6 @@
       "amd": true,
       "es6": true,
       "node": true
-    },
-    "globals": {
-      "showStat": false,
-      "attributeEntry": false,
-      "dictGet": false,
-      "getJSON": false,
-      "has_location": false,
-      "limit": false,
-      "localStorageTest": false,
-      "offline": false,
-      "one": false,
-      "online": false,
-      "showDistance": false,
-      "showTq": false,
-      "sortByKey": false,
-      "subtract": false,
-      "sum": false,
-      "trueDefault": false
     }
   }
 }
diff --git a/tasks/development.js b/tasks/development.js
index 933f123..d366a1c 100644
--- a/tasks/development.js
+++ b/tasks/development.js
@@ -4,9 +4,9 @@ module.exports = function (grunt) {
       server: {
         options: {
           base: {
-            path: 'build',
+            path: "build",
             options: {
-              index: 'index.html'
+              index: "index.html"
             }
           },
           livereload: true
@@ -18,7 +18,7 @@ module.exports = function (grunt) {
         options: {
           livereload: true
         },
-        files: ["*.css", "app.js", "helper.js", "lib/**/*.js", "*.html"],
+        files: ["*.css", "app.js", "lib/**/*.js", "*.html"],
         tasks: ["dev"]
       },
       config: {
diff --git a/tasks/linting.js b/tasks/linting.js
index de1b8d5..5409fd5 100644
--- a/tasks/linting.js
+++ b/tasks/linting.js
@@ -12,15 +12,6 @@ module.exports = function (grunt) {
       npm: {}
     },
     eslint: {
-      options: {
-        extends: [
-          "defaults/configurations/eslint"
-	],
-        rules: {
-          "semi": [2, "always"],
-          "no-undef": 0
-        }
-      },
       sources: {
         src: ["app.js", "!Gruntfile.js", "lib/**/*.js"]
       },

From 6869a19ea0b05001dbabc11aae9e14fe185be658 Mon Sep 17 00:00:00 2001
From: Xaver Maierhofer <xaver.maierhofer@xwissen.info>
Date: Thu, 26 May 2016 22:28:28 +0200
Subject: [PATCH 26/32] [TASK] Style layer selector

---
 assets/icons/fonts/hopglass.eot   | Bin 3352 -> 0 bytes
 assets/icons/fonts/hopglass.svg   |  27 ------------------------
 assets/icons/fonts/hopglass.ttf   | Bin 3184 -> 0 bytes
 assets/icons/fonts/hopglass.woff  | Bin 2300 -> 0 bytes
 assets/icons/fonts/hopglass.woff2 | Bin 1700 -> 0 bytes
 assets/icons/fonts/icon.ttf       | Bin 0 -> 3400 bytes
 assets/icons/fonts/icon.woff      | Bin 0 -> 2472 bytes
 assets/icons/fonts/icon.woff2     | Bin 0 -> 1760 bytes
 assets/icons/hopglass-icons.css   |  10 ++++++---
 scss/_leaflet-layer.scss          |  33 ++++++++++++++++++++++++++++++
 scss/main.scss                    |   1 +
 11 files changed, 41 insertions(+), 30 deletions(-)
 delete mode 100644 assets/icons/fonts/hopglass.eot
 delete mode 100644 assets/icons/fonts/hopglass.svg
 delete mode 100644 assets/icons/fonts/hopglass.ttf
 delete mode 100644 assets/icons/fonts/hopglass.woff
 delete mode 100644 assets/icons/fonts/hopglass.woff2
 create mode 100644 assets/icons/fonts/icon.ttf
 create mode 100644 assets/icons/fonts/icon.woff
 create mode 100644 assets/icons/fonts/icon.woff2
 create mode 100644 scss/_leaflet-layer.scss

diff --git a/assets/icons/fonts/hopglass.eot b/assets/icons/fonts/hopglass.eot
deleted file mode 100644
index c98534acce803009679a9b96251e6a6796287354..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 3352
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
B599y<

diff --git a/assets/icons/fonts/hopglass.svg b/assets/icons/fonts/hopglass.svg
deleted file mode 100644
index da13e93..0000000
--- a/assets/icons/fonts/hopglass.svg
+++ /dev/null
@@ -1,27 +0,0 @@
-<?xml version="1.0" standalone="no"?>
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg xmlns="http://www.w3.org/2000/svg">
-<metadata>Generated by Fontastic.me</metadata>
-<defs>
-<font id="hopglass" horiz-adv-x="512">
-<font-face font-family="hopglass" units-per-em="512" ascent="480" descent="-32"/>
-<missing-glyph horiz-adv-x="512" />
-
-<glyph glyph-name="android-add" unicode="&#62151;" d="M416 235l-139 0 0-139-42 0 0 139-139 0 0 42 139 0 0 139 42 0 0-139 139 0z"/>
-<glyph glyph-name="chevron-left" unicode="&#61732;" d="M214 256l167 174c4 4 4 11 0 16l-30 30c-4 5-11 5-16 1l-204-213c-2-2-3-5-3-8 0-3 1-6 3-8l204-213c5-4 12-4 16 0l30 31c4 4 4 11 0 16z"/>
-<glyph glyph-name="chevron-right" unicode="&#61733;" d="M298 256l-167 174c-4 4-4 11 0 16l30 30c5 5 12 5 16 1l204-213c2-2 3-5 3-8 0-3-1-6-3-8l-204-213c-4-4-11-4-16 0l-30 31c-4 4-4 11 0 16z"/>
-<glyph glyph-name="pin" unicode="&#62371;" d="M332 284c-1 0-3 1-4 2-7 4-12 11-14 19l-18 118 0 5c0 7 4 11 10 13 0 1 1 1 2 1 7 4 12 8 12 17 0 20-7 21-18 21l-92 0c-11 0-18-1-18-21 0-9 5-13 12-17 1 0 2 0 2-1 6-3 10-6 10-13l0-5-18-118c-2-8-7-15-14-19-1-1-2-2-4-2-19-10-36-31-36-55 0-16 4-21 15-21l81 0 12-176 8 0 12 176 81 0c12 0 15 5 15 21 0 24-16 45-36 55z"/>
-<glyph glyph-name="wifi" unicode="&#62044;" d="M256 416c-80 0-156-30-214-84l-10-10 10-9 32-32 10-10 9 9c45 41 102 64 163 64 60 0 118-23 163-64l9-9 10 10 32 32 10 9-10 10c-58 54-134 84-214 84z m141-160c-38 35-88 54-141 54l-9 0 0 0c-49-2-96-21-132-54l-11-10 10-10 33-32 9-9 10 8c25 22 57 35 90 35 33 0 65-13 91-35l9-8 9 9 33 32 10 10z m-141-160l10 9 53 53 10 10-11 10c-16 11-33 20-62 20-29 0-45-10-61-20l-12-10 11-10 53-53z"/>
-<glyph glyph-name="eye" unicode="&#61747;" d="M256 384c-82 0-146-49-224-128 67-68 124-128 224-128 100 0 173 76 224 127-52 58-125 129-224 129z m0-219c-49 0-90 41-90 91 0 50 41 91 90 91 49 0 90-41 90-91 0-50-41-91-90-91z m0 123c0 8 3 15 8 21-3 0-5 0-8 0-29 0-52-24-52-53 0-29 23-53 52-53 29 0 52 24 52 53 0 2 0 5 0 7-6-4-12-7-20-7-18 0-32 14-32 32z"/>
-<glyph glyph-name="ios-arrow-thin-left" unicode="&#62421;" d="M190 162c3 3 3 8 0 12l-67 74 285 0c4 0 8 4 8 8 0 4-4 8-8 8l-285 0 67 74c3 4 3 8 0 12-3 3-9 3-12 0 0 0-79-87-80-88 0-1-2-3-2-6 0-3 2-5 2-6 1-1 80-88 80-88 2-1 4-2 6-2 2 0 4 1 6 2z"/>
-<glyph glyph-name="ios-arrow-thin-right" unicode="&#62422;" d="M322 162c-3 3-3 8 0 12l67 74-285 0c-4 0-8 4-8 8 0 4 4 8 8 8l285 0-67 74c-3 4-3 8 0 12 3 3 9 3 12 0 0 0 79-87 80-88 0-1 2-3 2-6 0-3-2-5-2-6-1-1-80-88-80-88-2-1-4-2-6-2-2 0-4 1-6 2z"/>
-<glyph glyph-name="arrow-up-b" unicode="&#61709;" d="M413 185l-2 2-136 156c-4 6-11 9-19 9-8 0-14-4-19-9l-136-156-2-3c-2-2-3-5-3-8 0-9 7-16 17-16l286 0c10 0 17 7 17 16 0 3-1 6-3 9z"/>
-<glyph glyph-name="arrow-down-b" unicode="&#61700;" d="M99 327l2-2 136-156c4-6 11-9 19-9 8 0 15 3 19 9l136 156 2 2c2 3 3 6 3 9 0 9-7 16-17 16l-286 0c-10 0-17-7-17-16 0-3 1-6 3-9z"/>
-<glyph glyph-name="android-locate" unicode="&#62185;" d="M256 336c-44 0-80-36-80-80 0-44 36-80 80-80 44 0 80 36 80 80 0 44-36 80-80 80z m191-59c-10 89-81 160-170 170l0 33-42 0 0-33c-89-10-160-81-170-170l-33 0 0-42 33 0c10-89 81-160 170-170l0-33 42 0 0 33c89 10 160 81 170 170l33 0 0 42z m-191-170c-82 0-149 67-149 149 0 82 67 149 149 149 82 0 149-67 149-149 0-82-67-149-149-149z"/>
-<glyph glyph-name="android-close" unicode="&#62167;" d="M405 375l-30 30-119-119-119 119-30-30 119-119-119-119 30-30 119 119 119-119 30 30-119 119z"/>
-<glyph glyph-name="android-lock" unicode="&#62354;" d="M376 326l-20 0 0 40c0 55-45 100-100 100-55 0-100-45-100-100l0-40-20 0c-22 0-40-18-40-40l0-200c0-22 18-40 40-40l240 0c22 0 40 18 40 40l0 200c0 22-18 40-40 40z m-120-182c-22 0-40 18-40 40 0 22 18 40 40 40 22 0 40-18 40-40 0-22-18-40-40-40z m62 182l-124 0 0 40c0 34 28 62 62 62 34 0 62-28 62-62z"/>
-<glyph glyph-name="ios-copy" unicode="&#62492;" d="M144 96l0 304-32 0 0-336 240 0 0 32z m181 352l-165 0 0-336 240 0 0 261z m43-112l-80 0 0 80 16 0 0-64 64 0z"/>
-<glyph glyph-name="location" unicode="&#62550;" d="M256 448c-66 0-119-54-119-120 0-115 119-264 119-264 0 0 119 149 119 264 0 66-53 120-119 120z m0-178c-31 0-56 25-56 56 0 32 25 57 56 57 31 0 56-25 56-57 0-31-25-56-56-56z"/>
-<glyph glyph-name="android-remove" unicode="&#62196;" d="M96 277l320 0 0-42-320 0z"/>
-<glyph glyph-name="ios-person" unicode="&#62590;" d="M363 148c-13 5-31 6-43 9-7 1-17 5-20 9-3 4-1 41-1 41 0 0 6 10 9 18 4 8 7 31 7 31 0 0 7 0 9 12 3 13 7 19 6 28 0 9-5 10-5 10 0 0 5 13 5 42 1 34-25 68-74 68-49 0-75-34-74-68 0-28 5-42 5-42 0 0-5-1-5-10-1-9 3-14 6-27 2-12 9-12 9-12 0 0 4-24 7-32 3-8 9-18 9-18 0 0 2-37-1-41-3-4-13-8-20-9-12-3-30-4-43-9-13-5-53-20-53-52l320 0c0 32-40 47-53 52z"/>
-</font></defs></svg>
diff --git a/assets/icons/fonts/hopglass.ttf b/assets/icons/fonts/hopglass.ttf
deleted file mode 100644
index d117d0cd3e44fa916e7e29fbe10a9d730601881a..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 3184
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

diff --git a/assets/icons/fonts/hopglass.woff b/assets/icons/fonts/hopglass.woff
deleted file mode 100644
index b1a185a83939c6c289fd9d803986ba16ec3af579..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 2300
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

diff --git a/assets/icons/fonts/hopglass.woff2 b/assets/icons/fonts/hopglass.woff2
deleted file mode 100644
index cce227441683b22d72d300daabbcad81e633863a..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 1700
zcmV;V23z@ePew8T0RR9100yK04FCWD01S8l00vC}0RR9100000000000000000000
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

diff --git a/assets/icons/fonts/icon.ttf b/assets/icons/fonts/icon.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..8b8d8221cc5bfa2d9530bb615bce4ea55a9ed94d
GIT binary patch
literal 3400
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
Z5|%vFi$kN4OFM^mc3*!_C~W*6@ox!-B0c~B

literal 0
HcmV?d00001

diff --git a/assets/icons/fonts/icon.woff b/assets/icons/fonts/icon.woff
new file mode 100644
index 0000000000000000000000000000000000000000..7cc19f45434c92dfaadee96d32043182541c9272
GIT binary patch
literal 2472
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
DW%qBU

literal 0
HcmV?d00001

diff --git a/assets/icons/fonts/icon.woff2 b/assets/icons/fonts/icon.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..0377338e069c95378c91bcf8ce06f6efe4592d5a
GIT binary patch
literal 1760
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
C$3N--

literal 0
HcmV?d00001

diff --git a/assets/icons/hopglass-icons.css b/assets/icons/hopglass-icons.css
index f3ab178..8cfb80e 100644
--- a/assets/icons/hopglass-icons.css
+++ b/assets/icons/hopglass-icons.css
@@ -1,8 +1,8 @@
 @font-face {
     font-family: "Ionicons";
-    src: url("fonts/hopglass.woff2") format("woff2"),
-    url("fonts/hopglass.woff") format("woff"),
-    url("fonts/hopglass.ttf") format("truetype");
+    src: url("fonts/icon.woff2") format("woff2"),
+    url("fonts/icon.woff") format("woff"),
+    url("fonts/icon.ttf") format("truetype");
     font-weight: normal;
     font-style: normal;
 
@@ -90,3 +90,7 @@
 .ion-ios-person:before {
     content: "\f47e";
 }
+
+.ion-layer:before {
+    content: "\f229";
+}
diff --git a/scss/_leaflet-layer.scss b/scss/_leaflet-layer.scss
new file mode 100644
index 0000000..a22f74e
--- /dev/null
+++ b/scss/_leaflet-layer.scss
@@ -0,0 +1,33 @@
+.leaflet-control-layers {
+  box-shadow: none;
+  border-radius: 0;
+  background: none;
+}
+
+.leaflet-control-layers-toggle {
+  background: none;
+  &::before {
+    content: "\f229";
+    display: inline-block;
+    font-family: "ionicons" !important;
+    font-size: 2.3rem;
+    speak: none;
+    text-rendering: auto;
+    line-height: 1;
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+    color: #e32d6d;
+  }
+}
+
+.leaflet-control-layers-expanded {
+  padding: 0;
+}
+
+.leaflet-control-layers-list {
+  background: rgba(255, 255, 255, 0.9);
+  padding: 10px;
+  label {
+    cursor: pointer;
+  }
+}
diff --git a/scss/main.scss b/scss/main.scss
index 0415212..e1dd673 100644
--- a/scss/main.scss
+++ b/scss/main.scss
@@ -3,6 +3,7 @@
 @import '_base';
 @import '_leaflet';
 @import '_leaflet.label';
+@import '_leaflet-layer';
 @import '_filters';
 @import '_loader';
 

From 5c15ccf340e07945763a9daf889ea60a3bc6fbf9 Mon Sep 17 00:00:00 2001
From: Xaver Maierhofer <xaver.maierhofer@xwissen.info>
Date: Thu, 26 May 2016 22:29:21 +0200
Subject: [PATCH 27/32] [TASK] Add scss to grunt watcher

---
 tasks/development.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tasks/development.js b/tasks/development.js
index d366a1c..e9b312e 100644
--- a/tasks/development.js
+++ b/tasks/development.js
@@ -18,7 +18,7 @@ module.exports = function (grunt) {
         options: {
           livereload: true
         },
-        files: ["*.css", "app.js", "lib/**/*.js", "*.html"],
+        files: ["*.css", "app.js", "lib/**/*.js", "*.html", "scss/**/*.scss"],
         tasks: ["dev"]
       },
       config: {

From 9987e443f7e42f46c6c3b99062c6902ef8383d78 Mon Sep 17 00:00:00 2001
From: Xaver Maierhofer <xaver.maierhofer@xwissen.info>
Date: Fri, 27 May 2016 23:59:01 +0200
Subject: [PATCH 28/32] [TASK] Use strict

---
 Gruntfile.js               | 2 ++
 app.js                     | 2 ++
 bower.json                 | 2 +-
 lib/about.js               | 2 ++
 lib/container.js           | 2 ++
 lib/datadistributor.js     | 2 ++
 lib/filters/filtergui.js   | 4 +++-
 lib/filters/genericnode.js | 2 ++
 lib/filters/nodefilter.js  | 4 +++-
 lib/forcegraph.js          | 2 ++
 lib/gui.js                 | 2 ++
 lib/infobox/link.js        | 2 ++
 lib/infobox/location.js    | 2 ++
 lib/infobox/main.js        | 2 ++
 lib/infobox/node.js        | 2 ++
 lib/legend.js              | 2 ++
 lib/linklist.js            | 2 ++
 lib/locationmarker.js      | 2 ++
 lib/main.js                | 2 ++
 lib/map.js                 | 2 ++
 lib/map/clientlayer.js     | 1 +
 lib/map/labelslayer.js     | 2 ++
 lib/meshstats.js           | 2 ++
 lib/nodelist.js            | 2 ++
 lib/proportions.js         | 1 +
 lib/router.js              | 2 ++
 lib/sidebar.js             | 4 +++-
 lib/simplenodelist.js      | 2 ++
 lib/sorttable.js           | 2 ++
 lib/tabs.js                | 4 +++-
 lib/title.js               | 2 ++
 lib/utils/helper.js        | 4 +++-
 tasks/build.js             | 2 ++
 tasks/clean.js             | 2 ++
 tasks/development.js       | 2 ++
 tasks/linting.js           | 2 ++
 36 files changed, 74 insertions(+), 6 deletions(-)

diff --git a/Gruntfile.js b/Gruntfile.js
index 99fcd04..c508723 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -1,4 +1,6 @@
 module.exports = function (grunt) {
+  "use strict";
+
   grunt.loadTasks("tasks");
 
   grunt.registerTask("default", ["bower-install-simple", "lint", "copy", "sass", "postcss", "requirejs:default", "inline"]);
diff --git a/app.js b/app.js
index d6a1227..f2d4e3c 100644
--- a/app.js
+++ b/app.js
@@ -1,3 +1,5 @@
+"use strict";
+
 require.config({
   baseUrl: "lib",
   paths: {
diff --git a/bower.json b/bower.json
index 69d1b13..8233913 100644
--- a/bower.json
+++ b/bower.json
@@ -18,7 +18,7 @@
     "es6-shim": "~0.35.1",
     "almond": "~0.3.2",
     "d3": "~3.5.17",
-    "roboto-fontface": "~0.3.0",
+    "roboto-fontface": "~0.4.5",
     "virtual-dom": "~2.1.1",
     "leaflet-providers": "~1.1.10",
     "rbush": "https://github.com/mourner/rbush.git#~1.4.3"
diff --git a/lib/about.js b/lib/about.js
index 2a20ae5..9222794 100644
--- a/lib/about.js
+++ b/lib/about.js
@@ -1,4 +1,6 @@
 define(function () {
+  "use strict";
+
   return function () {
     this.render = function (d) {
       var el = document.createElement("div");
diff --git a/lib/container.js b/lib/container.js
index 4f32a84..d799167 100644
--- a/lib/container.js
+++ b/lib/container.js
@@ -1,4 +1,6 @@
 define([], function () {
+  "use strict";
+
   return function (tag) {
     if (!tag) {
       tag = "div";
diff --git a/lib/datadistributor.js b/lib/datadistributor.js
index 3edeaf6..ea79341 100644
--- a/lib/datadistributor.js
+++ b/lib/datadistributor.js
@@ -1,4 +1,6 @@
 define(["filters/nodefilter"], function (NodeFilter) {
+  "use strict";
+
   return function () {
     var targets = [];
     var filterObservers = [];
diff --git a/lib/filters/filtergui.js b/lib/filters/filtergui.js
index 10629d7..4db1b4b 100644
--- a/lib/filters/filtergui.js
+++ b/lib/filters/filtergui.js
@@ -1,4 +1,6 @@
-define([], function () {
+define(function () {
+  "use strict";
+
   return function (distributor) {
     var container = document.createElement("ul");
     container.classList.add("filters");
diff --git a/lib/filters/genericnode.js b/lib/filters/genericnode.js
index c4fe7a9..253c38b 100644
--- a/lib/filters/genericnode.js
+++ b/lib/filters/genericnode.js
@@ -1,4 +1,6 @@
 define(["helper"], function (helper) {
+  "use strict";
+
   return function (name, key, value, f) {
     var negate = false;
     var refresh;
diff --git a/lib/filters/nodefilter.js b/lib/filters/nodefilter.js
index 1d6bf5f..68da159 100644
--- a/lib/filters/nodefilter.js
+++ b/lib/filters/nodefilter.js
@@ -1,4 +1,6 @@
-define([], function () {
+define(function () {
+  "use strict";
+
   return function (filter) {
     return function (data) {
       var n = Object.create(data);
diff --git a/lib/forcegraph.js b/lib/forcegraph.js
index 645b053..a113345 100644
--- a/lib/forcegraph.js
+++ b/lib/forcegraph.js
@@ -1,4 +1,6 @@
 define(["d3", "helper"], function (d3, helper) {
+  "use strict";
+
   var margin = 200;
   var NODE_RADIUS = 15;
   var LINE_RADIUS = 7;
diff --git a/lib/gui.js b/lib/gui.js
index f8427ed..10c3bb2 100644
--- a/lib/gui.js
+++ b/lib/gui.js
@@ -5,6 +5,8 @@ define(["chroma-js", "map", "sidebar", "tabs", "container", "meshstats",
   function (chroma, Map, Sidebar, Tabs, Container, Meshstats, Legend, Linklist,
             Nodelist, SimpleNodelist, Infobox, Proportions, ForceGraph,
             Title, About, DataDistributor, FilterGUI) {
+    "use strict";
+
     return function (config, router) {
       var self = this;
       var content;
diff --git a/lib/infobox/link.js b/lib/infobox/link.js
index d24e144..8cfe683 100644
--- a/lib/infobox/link.js
+++ b/lib/infobox/link.js
@@ -1,4 +1,6 @@
 define(["helper"], function (helper) {
+  "use strict";
+
   function showStatImg(o, d) {
     var subst = {};
     subst["{SOURCE}"] = d.source.node_id;
diff --git a/lib/infobox/location.js b/lib/infobox/location.js
index 4695ac4..fe40011 100644
--- a/lib/infobox/location.js
+++ b/lib/infobox/location.js
@@ -1,4 +1,6 @@
 define(["helper"], function (helper) {
+  "use strict";
+
   return function (config, el, router, d) {
     var sidebarTitle = document.createElement("h2");
     sidebarTitle.textContent = "Location: " + d.toString();
diff --git a/lib/infobox/main.js b/lib/infobox/main.js
index 8f1ef4a..9690a2a 100644
--- a/lib/infobox/main.js
+++ b/lib/infobox/main.js
@@ -1,4 +1,6 @@
 define(["infobox/link", "infobox/node", "infobox/location"], function (Link, Node, Location) {
+  "use strict";
+
   return function (config, sidebar, router) {
     var self = this;
     var el;
diff --git a/lib/infobox/node.js b/lib/infobox/node.js
index 6839206..b6fcdac 100644
--- a/lib/infobox/node.js
+++ b/lib/infobox/node.js
@@ -1,5 +1,7 @@
 define(["moment", "tablesort", "helper", "moment.de"],
   function (moment, Tablesort, helper) {
+    "use strict";
+
     function showGeoURI(d) {
       function showLatitude(d) {
         var suffix = Math.sign(d) > -1 ? "' N" : "' S";
diff --git a/lib/legend.js b/lib/legend.js
index b01782f..20c0e97 100644
--- a/lib/legend.js
+++ b/lib/legend.js
@@ -1,4 +1,6 @@
 define(function () {
+  "use strict";
+
   return function () {
     var self = this;
 
diff --git a/lib/linklist.js b/lib/linklist.js
index 661b777..0bf6d7e 100644
--- a/lib/linklist.js
+++ b/lib/linklist.js
@@ -1,4 +1,6 @@
 define(["sorttable", "virtual-dom", "helper"], function (SortTable, V, helper) {
+  "use strict";
+
   function linkName(d) {
     return (d.source.node ? d.source.node.nodeinfo.hostname : d.source.id) + " – " + d.target.node.nodeinfo.hostname;
   }
diff --git a/lib/locationmarker.js b/lib/locationmarker.js
index 0f8c35a..a4a596f 100644
--- a/lib/locationmarker.js
+++ b/lib/locationmarker.js
@@ -1,4 +1,6 @@
 define(["leaflet"], function (L) {
+  "use strict";
+
   return L.CircleMarker.extend({
     outerCircle: {
       stroke: false,
diff --git a/lib/main.js b/lib/main.js
index 36d421c..693191c 100644
--- a/lib/main.js
+++ b/lib/main.js
@@ -1,5 +1,7 @@
 define(["moment", "router", "leaflet", "gui", "helper", "moment.de"],
   function (moment, Router, L, GUI, helper) {
+    "use strict";
+
     return function (config) {
       function handleData(data) {
         var dataNodes = {};
diff --git a/lib/map.js b/lib/map.js
index f099fb6..0bc17cb 100644
--- a/lib/map.js
+++ b/lib/map.js
@@ -2,6 +2,8 @@ define(["map/clientlayer", "map/labelslayer",
     "d3", "leaflet", "moment", "locationmarker", "rbush", "helper",
     "leaflet.label", "leaflet.providers", "moment.de"],
   function (ClientLayer, LabelsLayer, d3, L, moment, LocationMarker, rbush, helper) {
+    "use strict";
+
     var options = {
       worldCopyJump: true,
       zoomControl: false
diff --git a/lib/map/clientlayer.js b/lib/map/clientlayer.js
index 7579f4e..1dc8cb7 100644
--- a/lib/map/clientlayer.js
+++ b/lib/map/clientlayer.js
@@ -1,5 +1,6 @@
 define(["leaflet"],
   function (L) {
+    "use strict";
 
     return L.TileLayer.Canvas.extend({
       setData: function (d) {
diff --git a/lib/map/labelslayer.js b/lib/map/labelslayer.js
index a6af979..d54c4af 100644
--- a/lib/map/labelslayer.js
+++ b/lib/map/labelslayer.js
@@ -1,5 +1,7 @@
 define(["leaflet", "rbush"],
   function (L, rbush) {
+    "use strict";
+
     var labelLocations = [["left", "middle", 0 / 8],
       ["center", "top", 6 / 8],
       ["right", "middle", 4 / 8],
diff --git a/lib/meshstats.js b/lib/meshstats.js
index bc6c025..dd1d1f7 100644
--- a/lib/meshstats.js
+++ b/lib/meshstats.js
@@ -1,4 +1,6 @@
 define(["helper"], function (helper) {
+  "use strict";
+
   return function (config) {
     var self = this;
     var stats, timestamp;
diff --git a/lib/nodelist.js b/lib/nodelist.js
index 12687c4..471aba4 100644
--- a/lib/nodelist.js
+++ b/lib/nodelist.js
@@ -1,4 +1,6 @@
 define(["sorttable", "virtual-dom", "helper"], function (SortTable, V, helper) {
+  "use strict";
+
   function getUptime(now, d) {
     if (d.flags.online && "uptime" in d.statistics) {
       return Math.round(d.statistics.uptime);
diff --git a/lib/proportions.js b/lib/proportions.js
index 391145e..d8a9cda 100644
--- a/lib/proportions.js
+++ b/lib/proportions.js
@@ -1,5 +1,6 @@
 define(["chroma-js", "virtual-dom", "filters/genericnode", "helper"],
   function (Chroma, V, Filter, helper) {
+    "use strict";
 
     return function (config, filterManager) {
       var self = this;
diff --git a/lib/router.js b/lib/router.js
index d841d32..7c9fc4a 100644
--- a/lib/router.js
+++ b/lib/router.js
@@ -1,4 +1,6 @@
 define(["helper"], function (helper) {
+  "use strict";
+
   return function () {
     var self = this;
     var objects = {nodes: {}, links: {}};
diff --git a/lib/sidebar.js b/lib/sidebar.js
index 4c839cc..f644c6e 100644
--- a/lib/sidebar.js
+++ b/lib/sidebar.js
@@ -1,4 +1,6 @@
-define([], function () {
+define(function () {
+  "use strict";
+
   return function (el) {
     var self = this;
 
diff --git a/lib/simplenodelist.js b/lib/simplenodelist.js
index 2ba0207..be480d1 100644
--- a/lib/simplenodelist.js
+++ b/lib/simplenodelist.js
@@ -1,4 +1,6 @@
 define(["moment", "virtual-dom", "helper", "moment.de"], function (moment, V, helper) {
+  "use strict";
+
   return function (nodes, field, router, title) {
     var self = this;
     var el, tbody;
diff --git a/lib/sorttable.js b/lib/sorttable.js
index 881efba..2fc7353 100644
--- a/lib/sorttable.js
+++ b/lib/sorttable.js
@@ -1,4 +1,6 @@
 define(["virtual-dom"], function (V) {
+  "use strict";
+
   return function (headings, sortIndex, renderRow) {
     var data;
     var sortReverse = false;
diff --git a/lib/tabs.js b/lib/tabs.js
index f4fed6d..701af29 100644
--- a/lib/tabs.js
+++ b/lib/tabs.js
@@ -1,4 +1,6 @@
-define([], function () {
+define(function () {
+  "use strict";
+
   return function () {
     var self = this;
 
diff --git a/lib/title.js b/lib/title.js
index 4c99c2c..243c6bd 100644
--- a/lib/title.js
+++ b/lib/title.js
@@ -1,4 +1,6 @@
 define(function () {
+  "use strict";
+
   return function (config) {
     function setTitle(d) {
       var title = [config.siteName];
diff --git a/lib/utils/helper.js b/lib/utils/helper.js
index e3dce69..7a568c3 100644
--- a/lib/utils/helper.js
+++ b/lib/utils/helper.js
@@ -1,3 +1,5 @@
+"use strict";
+
 define({
   get: function (url) {
     return new Promise(function (resolve, reject) {
@@ -77,7 +79,7 @@ define({
   },
 
   listReplace: function (s, subst) {
-    for (key in subst) {
+    for (var key in subst) {
       var re = new RegExp(key, "g");
       s = s.replace(re, subst[key]);
     }
diff --git a/tasks/build.js b/tasks/build.js
index 3a70fe8..4701cfb 100644
--- a/tasks/build.js
+++ b/tasks/build.js
@@ -1,4 +1,6 @@
 module.exports = function (grunt) {
+  "use strict";
+
   grunt.config.merge({
     bowerdir: "bower_components",
     copy: {
diff --git a/tasks/clean.js b/tasks/clean.js
index ed0e234..67181e0 100644
--- a/tasks/clean.js
+++ b/tasks/clean.js
@@ -1,4 +1,6 @@
 module.exports = function (grunt) {
+  "use strict";
+
   grunt.config.merge({
     clean: {
       build: ["build/**/*", "node_modules/grunt-newer/.cache"]
diff --git a/tasks/development.js b/tasks/development.js
index e9b312e..938cd87 100644
--- a/tasks/development.js
+++ b/tasks/development.js
@@ -1,4 +1,6 @@
 module.exports = function (grunt) {
+  "use strict";
+
   grunt.config.merge({
     connect: {
       server: {
diff --git a/tasks/linting.js b/tasks/linting.js
index 5409fd5..6667d68 100644
--- a/tasks/linting.js
+++ b/tasks/linting.js
@@ -1,4 +1,6 @@
 module.exports = function (grunt) {
+  "use strict";
+
   grunt.config.merge({
     checkDependencies: {
       options: {

From 4c26a8c2c9151a8a2ea8b3e4004a6015d81b2c12 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Milan=20P=C3=A4ssler?= <me@petabyteboy.de>
Date: Sat, 18 Mar 2017 16:29:30 +0100
Subject: [PATCH 29/32] [TASK] Add required information into package.json

---
 package.json | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/package.json b/package.json
index caa6d5c..288b031 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,13 @@
 {
   "name": "hopglass",
+  "license": "AGPL-3.0",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/hopglass/hopglass.git"
+  },
+  "bugs": {
+    "url": "https://github.com/hopglass/hopglass/issues"
+  },
   "scripts": {
     "test": "node -e \"require('grunt').cli()\" '' clean lint"
   },

From 4171594782482986121d1b0a59b63d8b2d0ff036 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Milan=20P=C3=A4ssler?= <me@petabyteboy.de>
Date: Sat, 18 Mar 2017 16:29:47 +0100
Subject: [PATCH 30/32] [BUGFIX] bower package name should be lowercase

---
 bower.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/bower.json b/bower.json
index 8233913..18464c2 100644
--- a/bower.json
+++ b/bower.json
@@ -1,5 +1,5 @@
 {
-  "name": "HopGlass",
+  "name": "hopglass",
   "ignore": [
     "node_modules",
     "bower_components",

From 27704d4e3e03f201b08423b502d55de1efd4004c Mon Sep 17 00:00:00 2001
From: Xaver Maierhofer <xmaierhofer@1drop.de>
Date: Sun, 29 May 2016 00:50:48 +0200
Subject: [PATCH 31/32] [TASK] Update eslint to 2.11.0

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 288b031..8996160 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,7 @@
   },
   "devDependencies": {
     "autoprefixer": "^6.3.6",
-    "eslint": "^2.10.2",
+    "eslint": "^2.11.0",
     "eslint-config-defaults": "^9.0.0",
     "grunt": "^1.0.1",
     "grunt-bower-install-simple": "^1.2.3",

From bdde93a2c8c7fa4ef58657b692a404b2657a024b Mon Sep 17 00:00:00 2001
From: Milan Paessler <me@petabyteboy.de>
Date: Tue, 21 Mar 2017 22:56:16 +0100
Subject: [PATCH 32/32] [TASK] travis: bump node version

---
 .travis.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.travis.yml b/.travis.yml
index 50cef3f..5f3eab1 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,4 +1,5 @@
 language: node_js
+node_js: node
 before_install:
   - gem install sass
   - npm install -g grunt-cli