diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000000000000000000000000000000000000..f0764d2584242aa27a1beb3b6043fb215d4664e1
--- /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/app.js b/app.js
index 899049aa884b5abb26e37e2c0a11a98efa8fe1a0..d6a1227046a4d497727f361e8b2ae5480be74c8b 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/helper.js b/helper.js
deleted file mode 100644
index 5ded2f77dc8c69b8fd4362e6254482b97a4fa4ec..0000000000000000000000000000000000000000
--- a/helper.js
+++ /dev/null
@@ -1,235 +0,0 @@
-function get(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();
-  });
-}
-
-function getJSON(url) {
-  return get(url).then(JSON.parse)
-}
-
-function sortByKey(key, d) {
-  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)
-  })
-}
-
-function sum(a) {
-  return a.reduce(function (a, b) {
-    return a + b
-  }, 0)
-}
-
-function one() {
-  return 1
-}
-
-function trueDefault(d) {
-  return d === undefined ? true : d
-}
-
-function dictGet(dict, key) {
-  var k = key.shift();
-
-  if (!(k in dict)) {
-    return null;
-    }
-
-  if (key.length == 0) {
-    return dict[k];
-    }
-
-  return dictGet(dict[k], key)
-}
-
-function localStorageTest() {
-  var test = 'test';
-  try {
-    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])
-  }
-  return s
-}
-
-/* Helpers working with nodes */
-
-function offline(d) {
-  return !d.flags.online
-}
-
-function online(d) {
-  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
-}
-
-function subtract(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 */
-
-function showDistance(d) {
-  if (isNaN(d.distance)) {
-    return;
-    }
-
-  return d.distance.toFixed(0) + " m"
-}
-
-function showTq(d) {
-  return (1 / d.tq * 100).toFixed(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");
-  th.textContent = label;
-  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
-}
-
-function createIframe(opt, width, height) {
-  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
-}
-
-function showStat(o, subst) {
-  var content, caption;
-  subst = typeof subst !== 'undefined' ? subst : {};
-
-  if (o.thumbnail) {
-    content = document.createElement("img");
-    content.src = listReplace(o.thumbnail, subst)
-  }
-
-  if (o.caption) {
-    caption = listReplace(o.caption, subst);
-
-    if (!content) {
-      content = document.createTextNode(caption)
-      }
-  }
-
-  if (o.iframe) {
-    content = createIframe(o.iframe, o.width, o.height);
-    if (o.iframe.src) {
-      content.src = listReplace(o.iframe.src, subst);
-    } else {
-      content.src = listReplace(o.iframe, subst)
-      }
-  }
-
-  var p = document.createElement("p");
-
-  if (o.href) {
-    var link = document.createElement("a");
-    link.target = "_blank";
-    link.href = listReplace(o.href, subst);
-    link.appendChild(content);
-
-    if (caption && o.thumbnail) {
-      link.title = caption;
-    }
-
-    p.appendChild(link)
-  } else {
-        p.appendChild(content);
-    }
-
-  return p
-}
-
diff --git a/lib/about.js b/lib/about.js
index f888de1ea2214b27c2edf7d4bed3f4cd4943f10f..5c2af90e2f5fe01ded6418d6651ada387c562e5a 100644
--- a/lib/about.js
+++ b/lib/about.js
@@ -3,7 +3,7 @@ define(function () {
     this.render = function (d) {
       var el = document.createElement("div");
       d.appendChild(el);
-      var s = "<h2>Über HopGlass</h2>";
+      var s = "<h2>Über Mehsviewer</h2>";
 
       s += "<p>Mit Doppelklick und Shift+Doppelklick kann man in der Karte ";
       s += "auch zoomen.</p>";
diff --git a/lib/filters/genericnode.js b/lib/filters/genericnode.js
index 831f57558c511a9fd99597f5d0babe70195b0a6e..c4fe7a9ea82cad0884b1861fcb339f3c42ea7e2c 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 41c8e696e05dcda7cef0bae0d162e524bce9e155..84fb39411cf963fa6d192a0218d364867ea11e10 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 = 12;
@@ -32,7 +32,7 @@ define(["d3"], function (d3) {
     }
 
     function savePositions() {
-      if (!localStorageTest()) {
+      if (!helper.localStorageTest()) {
         return;
       }
 
@@ -750,7 +750,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 66f75fcc8b2a71f6a9859e79fc789314271048b0..1f6697e02ac9a547268ce2d35bc974e1de1afaa0 100644
--- a/lib/infobox/link.js
+++ b/lib/infobox/link.js
@@ -1,9 +1,9 @@
-define(function () {
+define(["helper"], function (helper) {
   function showStatImg(o, source, target) {
     var subst = {};
     subst["{SOURCE}"] = source;
     subst["{TARGET}"] = target;
-    return showStat(o, subst);
+    return helper.showStat(o, subst);
   }
 
   return function (config, el, router, d) {
@@ -17,7 +17,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);
@@ -28,11 +28,11 @@ define(function () {
     var attributes = document.createElement("table");
     attributes.classList.add("attributes");
 
-    attributeEntry(attributes, "TQ", showTq(d));
-    attributeEntry(attributes, "Entfernung", showDistance(d));
-    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));
+    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 9910d33eebe1d4b9a270c0c031165eed5b7804a5..4695ac4f28ccbf927a586d7ac5644c0cbb62b8d9 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 dc900e4384da6102ce3a12d8db3e34ff8fe60380..8f1ef4a48f88a366f9174abcf57e25903096966d 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 a1b2814ad467be907e89245ea7b09fc0aeaf5347..a906ff521ef294bd5ba8901fc1a90a058f609c4e 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) {
@@ -105,7 +105,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;
       }
@@ -193,7 +193,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;
       }
@@ -227,7 +227,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;
       }
@@ -238,8 +238,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) {
@@ -250,28 +250,28 @@ define(["moment", "tablesort", "moment.de"],
       var attributes = document.createElement("table");
       attributes.classList.add("attributes");
 
-      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, "Systemlast", showLoad(d));
-      attributeEntry(attributes, "Arbeitsspeicher", showRAM(d));
-      attributeEntry(attributes, "IP Adressen", showIPs(d));
-      attributeEntry(attributes, "Webseite", showPages(d));
-      attributeEntry(attributes, "Gewähltes Gateway", dictGet(d.statistics, ["gateway"]));
-      attributeEntry(attributes, "Autom. Updates", showAutoupdate(d));
-      attributeEntry(attributes, "Clients", showClients(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, "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", helper.dictGet(d.statistics, ["gateway"]));
+      helper.attributeEntry(attributes, "Autom. Updates", showAutoupdate(d));
+      helper.attributeEntry(attributes, "Clients", showClients(d));
 
       el.appendChild(attributes);
 
@@ -320,7 +320,7 @@ 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);
 
@@ -334,7 +334,7 @@ define(["moment", "tablesort", "moment.de"],
           a1.onclick = router.node(d.node);
           td2.appendChild(a1);
 
-          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");
@@ -346,7 +346,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);
@@ -354,7 +354,7 @@ define(["moment", "tablesort", "moment.de"],
           var td4 = 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);
           td4.appendChild(a4);
           td4.setAttribute("data-sort", d.link.distance !== undefined ? -d.link.distance : 1);
@@ -366,7 +366,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 b26d45ff5eb59060171495e07dfb2097651dd27f..597bc6d583b4c56d8a77b011770213f1ba574dec 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;
   }
@@ -33,8 +33,8 @@ 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 td3 = V.h("td", showDistance(d));
+      var td2 = V.h("td", {style: {color: linkScale(d.tq).hex()}}, helper.showTq(d));
+      var td3 = V.h("td", helper.showDistance(d));
 
       return V.h("tr", [td1, td2, td3]);
     }
diff --git a/lib/main.js b/lib/main.js
index 14cd1a15904fdc265e81f40670649a87413d8586..e5bb777885ef4e9c815278d5c4420bccdb46a2d6 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 = {};
@@ -19,7 +19,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);
@@ -28,7 +28,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 {
             dataNodes.nodes = dataNodes.nodes.concat(data[i].nodes);
             dataNodes.timestamp = data[i].timestamp;
@@ -47,8 +47,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 = {};
 
@@ -154,6 +154,7 @@ define(["moment", "router", "leaflet", "gui", "moment.de"],
           }
         };
       }
+
       moment.locale("de");
 
       var router = new Router();
@@ -170,7 +171,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);
       }
 
@@ -190,7 +191,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 3c7c6bc413dd03bc42a0132e2b1e7bbc6b39f2a3..b36ac13d91c4fbea223bbcc35ee1dac3393ad393 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;
@@ -275,16 +275,16 @@ 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) {
-          console.log(e);
+          console.error(e);
         }
       }
 
       function contextMenuOpenLayerMenu() {
-        document.querySelector('.leaflet-control-layers').classList.add('leaflet-control-layers-expanded');
+        document.querySelector(".leaflet-control-layers").classList.add("leaflet-control-layers-expanded");
       }
 
       var el = document.createElement("div");
@@ -322,7 +322,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) {
@@ -353,7 +353,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}));
         }
       });
@@ -504,25 +504,25 @@ define(["map/clientlayer", "map/labelslayer",
           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;
@@ -540,14 +540,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 57ec349c4c9d205f6f2d8e777f725293c4bc4ea4..0501870ab2902df55562ad6ecb2c8242818ff738 100644
--- a/lib/meshstats.js
+++ b/lib/meshstats.js
@@ -1,19 +1,19 @@
-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 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) {
+      var totalNodes = helper.sum(d.nodes.all.map(helper.one));
+      var totalOnlineNodes = helper.sum(d.nodes.all.filter(helper.online).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(d.nodes.all.filter(online).filter(function (d) {
+      var totalGateways = helper.sum(d.nodes.all.filter(helper.online).filter(function (d) {
         return d.flags.gateway;
-      }).map(one));
+      }).map(helper.one));
 
       var nodetext = [{count: totalOnlineNodes, label: "online"},
         {count: totalNewNodes, label: "neu"},
diff --git a/lib/nodelist.js b/lib/nodelist.js
index cd55254d93931d221be518cd9da2738b3a24ab09..c5d685a9ceb7df1534f44528041350d9f1c72a1f 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);
@@ -63,7 +63,7 @@ define(["sorttable", "virtual-dom"], function (SortTable, V) {
         href: "#"
       }, 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 6df291bbd46ad7bc221dc219ee75b553994fc169..67568299e0aa42ebf26eaee7c3ec3191e48193cd 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;
@@ -24,14 +24,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);
@@ -96,7 +96,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 = {};
 
@@ -139,8 +139,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 f719cc0ea0ee603abb965f4bcf1656ac9868e978..d841d32e05eb2590e4265b6d634319a6d1725fef 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 82f017a4b390276b6cf645e727dba1059f317eed..8ed0666d69fd2876a70fef01e5eb7b4dc3783348 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: "#"
         }, 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 0000000000000000000000000000000000000000..703f86a5698dde8353edf70e475ebc776c86c0e4
--- /dev/null
+++ b/lib/utils/helper.js
@@ -0,0 +1,234 @@
+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");
+    th.textContent = label;
+    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 c1129bd493987f539351b86430a421fd30eeca6b..88473beb17ad1747a148b3a7b8e33951e8366013 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 933f123c8beb67357a29a591934b875a1213ccfd..d366a1c130456cb48dcdcc7172bf313a97ceb875 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 8c435688419254a47871980e8aaeee04e265ccc2..5409fd58e664a8f5e7bf15d44143e60fcf7248f4 100644
--- a/tasks/linting.js
+++ b/tasks/linting.js
@@ -12,16 +12,6 @@ module.exports = function (grunt) {
       npm: {}
     },
     eslint: {
-      options: {
-        rules: {
-          "strict": [2, "never"],
-          "no-multi-spaces": 0,
-          "no-new": 0,
-          "no-shadow": 0,
-          "no-use-before-define": [1, "nofunc"],
-          "no-underscore-dangle": 0
-        }
-      },
       sources: {
         src: ["app.js", "!Gruntfile.js", "lib/**/*.js"]
       },