diff --git a/app.js b/app.js
index 5390671cae40fa61a8e2c319641a6ab78e408a12..84316b2df83915be46d4aacd64bea688c7a44be0 100644
--- a/app.js
+++ b/app.js
@@ -37,5 +37,7 @@ require.config({
 });
 
 require(['main'], function (main) {
-  main(jsonData);
+  /** global: config */
+  window.config = jsonData;
+  main();
 });
diff --git a/config.default.json b/config.default.json
index 39f1abc33b53da3419a78cc7fdeb8f91d7feb49d..f59737dd1ff80bd50dfb3ff7fe330eff6930d7aa 100644
--- a/config.default.json
+++ b/config.default.json
@@ -6,10 +6,69 @@
   "nodeZoom": 18,
   "labelZoom": 13,
   "clientZoom": 15,
-  "nodeInfobox": {
-    "contact": false,
-    "hardwareUsage": true
-  },
+  "nodeAttr": [
+    // value can be a node attribute (1 depth) or a a function in utils/node with prefix show
+    {
+      "name": "node.status",
+      "value": "Status"
+    },
+    {
+      "name": "node.gateway",
+      "value": "Gateway"
+    },
+    {
+      "name": "node.coordinates",
+      "value": "GeoURI"
+    },
+//    {
+//      "name": "node.contact",
+//      "value": "owner"
+//    },
+    {
+      "name": "node.hardware",
+      "value": "model"
+    },
+    {
+      "name": "node.primaryMac",
+      "value": "mac"
+    },
+    {
+      "name": "node.firmware",
+      "value": "Firmware"
+    },
+    {
+      "name": "node.uptime",
+      "value": "Uptime"
+    },
+    {
+      "name": "node.firstSeen",
+      "value": "FirstSeen"
+    },
+    {
+      "name": "node.systemLoad",
+      "value": "Load"
+    },
+    {
+      "name": "node.ram",
+      "value": "RAM"
+    },
+    {
+      "name": "node.ipAddresses",
+      "value": "IPs"
+    },
+    {
+      "name": "node.update",
+      "value": "Autoupdate"
+    },
+    {
+      "name": "node.site",
+      "value": "Site"
+    },
+    {
+      "name": "node.clients",
+      "value": "Clients"
+    }
+  ],
   "supportedLocale": [
     "en",
     "de",
diff --git a/lib/forcegraph.js b/lib/forcegraph.js
index ceecedc4578a8f21f64592f6007c37c27d6e82e6..b96010c6baaa447836031e76994822fb97f30e24 100644
--- a/lib/forcegraph.js
+++ b/lib/forcegraph.js
@@ -2,7 +2,7 @@ define(['d3-selection', 'd3-force', 'd3-zoom', 'd3-drag', 'd3-timer', 'd3-ease',
   function (d3Selection, d3Force, d3Zoom, d3Drag, d3Timer, d3Ease, d3Interpolate, math, draw) {
     'use strict';
 
-    return function (config, linkScale, sidebar, router) {
+    return function (linkScale, sidebar) {
       var self = this;
       var el;
       var canvas;
diff --git a/lib/gui.js b/lib/gui.js
index 01736b9b7ef0a5ebb2bfbaaf6b6eae4bf2679db4..a6d5639d0f2d3f99350ea1dedba45a20f50d53e5 100644
--- a/lib/gui.js
+++ b/lib/gui.js
@@ -7,7 +7,7 @@ function (d3Interpolate, Map, Sidebar, Tabs, Container, Legend, Linklist,
   Title, About, DataDistributor, FilterGUI, HostnameFilter) {
   'use strict';
 
-  return function (config, router, language) {
+  return function (language) {
     var self = this;
     var content;
     var contentDiv;
@@ -38,7 +38,7 @@ function (d3Interpolate, Map, Sidebar, Tabs, Container, Legend, Linklist,
     function addContent(K) {
       removeContent();
 
-      content = new K(config, linkScale, sidebar.getWidth, router, buttons);
+      content = new K(linkScale, sidebar.getWidth, buttons);
       content.render(contentDiv);
 
       fanout.add(content);
@@ -77,18 +77,18 @@ function (d3Interpolate, Map, Sidebar, Tabs, Container, Legend, Linklist,
 
     buttons.appendChild(buttonToggle);
 
-    var title = new Title(config);
+    var title = new Title();
 
     var header = new Container('header');
-    var infobox = new Infobox(config, sidebar, router, linkScale);
+    var infobox = new Infobox(sidebar, linkScale);
     var tabs = new Tabs();
     var overview = new Container();
-    var legend = new Legend(config, language);
-    var newnodeslist = new SimpleNodelist('new', 'firstseen', router, _.t('node.new'));
-    var lostnodeslist = new SimpleNodelist('lost', 'lastseen', router, _.t('node.missing'));
-    var nodelist = new Nodelist(router);
-    var linklist = new Linklist(linkScale, router);
-    var statistics = new Proportions(config, fanout);
+    var legend = new Legend(language);
+    var newnodeslist = new SimpleNodelist('new', 'firstseen', _.t('node.new'));
+    var lostnodeslist = new SimpleNodelist('lost', 'lastseen',  _.t('node.missing'));
+    var nodelist = new Nodelist();
+    var linklist = new Linklist(linkScale);
+    var statistics = new Proportions(fanout);
     var about = new About();
 
     fanoutUnfiltered.add(legend);
diff --git a/lib/infobox/link.js b/lib/infobox/link.js
index 521a7f354fdd8149aa7c379ad0acbd44cdc3b2b6..27e382774ceaf751f20aa478bc17e89ee79ea117 100644
--- a/lib/infobox/link.js
+++ b/lib/infobox/link.js
@@ -13,7 +13,7 @@ define(['helper', 'snabbdom'], function (helper, V) {
     return helper.showStat(V, o, subst);
   }
 
-  return function (config, el, router, d, linkScale) {
+  return function (el, d, linkScale) {
     var self = this;
     var header = document.createElement('div');
     var table = document.createElement('table');
diff --git a/lib/infobox/location.js b/lib/infobox/location.js
index e313f832ad1a2b12b9bf60de0bddd8abba5e81f3..cc4bcf5a4ab0a495a5fc1285818b8ad9d0417fbf 100644
--- a/lib/infobox/location.js
+++ b/lib/infobox/location.js
@@ -1,7 +1,7 @@
 define(['helper'], function (helper) {
   'use strict';
 
-  return function (config, el, router, d) {
+  return function (el, d) {
     var sidebarTitle = document.createElement('h2');
     sidebarTitle.textContent = _.t('location.location');
     el.appendChild(sidebarTitle);
diff --git a/lib/infobox/main.js b/lib/infobox/main.js
index f2984b7de76abb4816dd6aceb82b9c9d0b2a227a..c2076e49a297d3cc746463ef40f462229e96688a 100644
--- a/lib/infobox/main.js
+++ b/lib/infobox/main.js
@@ -1,7 +1,7 @@
 define(['infobox/link', 'infobox/node', 'infobox/location'], function (Link, Node, location) {
   'use strict';
 
-  return function (config, sidebar, router, linkScale) {
+  return function (sidebar, linkScale) {
     var self = this;
     var el;
     var node;
@@ -41,19 +41,19 @@ define(['infobox/link', 'infobox/node', 'infobox/location'], function (Link, Nod
 
     self.gotoNode = function gotoNode(d, nodeDict) {
       create();
-      node = new Node(config, el, router, d, linkScale, nodeDict);
+      node = new Node(el, d, linkScale, nodeDict);
       node.render();
     };
 
     self.gotoLink = function gotoLink(d) {
       create();
-      link = new Link(config, el, router, d, linkScale);
+      link = new Link(el, d, linkScale);
       link.render();
     };
 
     self.gotoLocation = function gotoLocation(d) {
       create();
-      location(config, el, router, d);
+      location(el, d);
     };
 
     self.setData = function setData(d) {
diff --git a/lib/infobox/node.js b/lib/infobox/node.js
index b08c8f9ef68a4c13fbcc100c56ea5fb0a8b93f5d..e9971060c27c4864e377d722db51fe8b5e309d2f 100644
--- a/lib/infobox/node.js
+++ b/lib/infobox/node.js
@@ -1,137 +1,8 @@
-define(['sorttable', 'snabbdom', 'd3-interpolate', 'moment', 'helper'],
-  function (SortTable, V, d3Interpolate, moment, helper) {
+define(['sorttable', 'snabbdom', 'd3-interpolate', 'moment', 'helper', 'utils/node'],
+  function (SortTable, V, d3Interpolate, moment, helper, nodef) {
     'use strict';
     V = V.default;
 
-    function showGeoURI(d) {
-      if (!helper.hasLocation(d)) {
-        return undefined;
-      }
-
-      return V.h('td',
-        V.h('a',
-          { props: { href: 'geo:' + d.location.latitude + ',' + d.location.longitude } },
-          Number(d.location.latitude.toFixed(6)) + ', ' + Number(d.location.longitude.toFixed(6))
-        )
-      );
-    }
-
-    function showStatus(d) {
-      return V.h('td',
-        { props: { className: d.is_online ? 'online' : 'offline' } },
-        _.t((d.is_online ? 'node.lastOnline' : 'node.lastOffline'), {
-          time: d.lastseen.fromNow(),
-          date: d.lastseen.format('DD.MM.YYYY, H:mm:ss')
-        }));
-    }
-
-    function showFirmware(d) {
-      return [
-        helper.dictGet(d, ['firmware', 'release']),
-        helper.dictGet(d, ['firmware', 'base'])
-      ].filter(function (n) {
-        return n !== null;
-      }).join(' / ') || undefined;
-    }
-
-    function showSite(d, config) {
-      var rt = d.site_code;
-      if (config.siteNames) {
-        config.siteNames.forEach(function (t) {
-          if (d.site_code === t.site) {
-            rt = t.name;
-          }
-        });
-      }
-      return rt;
-    }
-
-    function showClients(d) {
-      if (!d.is_online) {
-        return undefined;
-      }
-
-      var clients = [
-        V.h('span', [
-          d.clients > 0 ? d.clients : _.t('none'),
-          V.h('br'),
-          V.h('i', { props: { className: 'ion-people', title: _.t('node.clients') } })
-        ]),
-        V.h('span',
-          { props: { className: 'legend-24ghz' } },
-          [
-            d.clients_wifi24,
-            V.h('br'),
-            V.h('span', { props: { className: 'symbol', title: '2,4 Ghz' } })
-          ]),
-        V.h('span',
-          { props: { className: 'legend-5ghz' } },
-          [
-            d.clients_wifi5,
-            V.h('br'),
-            V.h('span', { props: { className: 'symbol', title: '5 Ghz' } })
-          ]),
-        V.h('span',
-          { props: { className: 'legend-others' } },
-          [
-            d.clients_other,
-            V.h('br'),
-            V.h('span', { props: { className: 'symbol', title: _.t('others') } })
-          ])
-      ];
-
-      return V.h('td', { props: { className: 'clients' } }, clients);
-    }
-
-    function showIPs(d) {
-      var string = [];
-      var ips = d.network.addresses;
-      ips.sort();
-      ips.forEach(function (ip, i) {
-        if (i > 0) {
-          string.push(V.h('br'));
-        }
-
-        if (ip.indexOf('fe80:') !== 0) {
-          string.push(V.h('a', { props: { href: 'http://[' + ip + ']/', target: '_blank' } }, ip));
-        } else {
-          string.push(ip);
-        }
-      });
-      return V.h('td', string);
-    }
-
-    function showBar(v, width, warning) {
-      return V.h('span',
-        { props: { className: 'bar' + (warning ? ' warning' : '') } },
-        [
-          V.h('span',
-            {
-              style: { width: (width * 100) + '%' }
-            }),
-          V.h('label', v)
-        ]
-      );
-    }
-
-    function showLoad(d) {
-      if (!('loadavg' in d)) {
-        return undefined;
-      }
-      return showBar(d.loadavg.toFixed(2), d.loadavg % 1, d.loadavg >= d.nproc);
-    }
-
-    function showRAM(d) {
-      if (!('memory_usage' in d)) {
-        return undefined;
-      }
-      return showBar(Math.round(d.memory_usage * 100) + ' %', d.memory_usage, d.memory_usage >= 0.8);
-    }
-
-    function showAutoupdate(d) {
-      return d.autoupdater.enabled ? _.t('node.activated', { branch: d.autoupdater.branch }) : _.t('node.deactivated');
-    }
-
     function showStatImg(o, d) {
       var subst = {};
       subst['{NODE_ID}'] = d.node_id;
@@ -141,7 +12,7 @@ define(['sorttable', 'snabbdom', 'd3-interpolate', 'moment', 'helper'],
       return helper.showStat(V, o, subst);
     }
 
-    return function (config, el, router, d, linkScale, nodeDict) {
+    return function (el, d, linkScale, nodeDict) {
       function nodeLink(node) {
         return V.h('a', {
           props: {
@@ -249,23 +120,24 @@ define(['sorttable', 'snabbdom', 'd3-interpolate', 'moment', 'helper'],
 
         var children = [];
 
-        children.push(helper.attributeEntry(V, 'node.status', showStatus(d)));
-        children.push(helper.attributeEntry(V, 'node.gateway', d.is_gateway ? 'ja' : undefined));
-        children.push(helper.attributeEntry(V, 'node.coordinates', showGeoURI(d)));
-        children.push(helper.attributeEntry(V, 'node.contact', d.owner));
-        children.push(helper.attributeEntry(V, 'node.hardware', d.model));
-        children.push(helper.attributeEntry(V, 'node.primaryMac', d.network.mac));
-        children.push(helper.attributeEntry(V, 'node.firmware', showFirmware(d)));
-        children.push(helper.attributeEntry(V, 'node.site', showSite(d, config)));
-        children.push(helper.attributeEntry(V, 'node.uptime', moment.utc(d.uptime).local().fromNow(true)));
-        children.push(helper.attributeEntry(V, 'node.firstSeen', d.firstseen.fromNow(true)));
-        if (config.nodeInfobox && config.nodeInfobox.hardwareUsage) {
-          children.push(helper.attributeEntry(V, 'node.systemLoad', showLoad(d)));
-          children.push(helper.attributeEntry(V, 'node.ram', showRAM(d)));
-        }
-        children.push(helper.attributeEntry(V, 'node.ipAddresses', showIPs(d)));
-        children.push(helper.attributeEntry(V, 'node.update', showAutoupdate(d)));
-        children.push(helper.attributeEntry(V, 'node.clients', showClients(d)));
+        config.nodeAttr.forEach(function (row) {
+          var field = d[row.value];
+          if (nodef['show' + row.value] !== undefined) {
+            field = nodef['show' + row.value](d);
+          }
+
+          if (field) {
+            if (typeof field !== 'object') {
+              field = V.h('td', field);
+            }
+
+            children.push(V.h('tr', [
+              V.h('th', _.t(row.name)),
+              field
+            ]));
+          }
+        });
+
         children.push(helper.attributeEntry(V, 'node.gateway', showGateway(d)));
 
         var elNew = V.h('table', children);
diff --git a/lib/legend.js b/lib/legend.js
index 549b87a3eddc289f5053974d71411b00ffd38785..71c1011ca00d95ca7d280cf28bd28a370b185f61 100644
--- a/lib/legend.js
+++ b/lib/legend.js
@@ -1,7 +1,7 @@
 define(['helper'], function (helper) {
   'use strict';
 
-  return function (config, language) {
+  return function (language) {
     var self = this;
     var stats = document.createTextNode('');
     var timestamp = document.createTextNode('');
diff --git a/lib/linklist.js b/lib/linklist.js
index b6d2cf3f2e702dd1ef3cc5838a014b931953c056..20cc3641a44436d30b078c29442b58a5b1f9cc58 100644
--- a/lib/linklist.js
+++ b/lib/linklist.js
@@ -28,7 +28,7 @@ define(['sorttable', 'snabbdom', 'helper'], function (SortTable, V, helper) {
     reverse: true
   }];
 
-  return function (linkScale, router) {
+  return function (linkScale) {
     var table = new SortTable(headings, 2, renderRow);
     V = V.default;
 
diff --git a/lib/main.js b/lib/main.js
index 45bf4f083429d1fba732724ae07166905e4d9567..bcd9a8bc0eb964504e2a7683fbf409a3e6e7703b 100644
--- a/lib/main.js
+++ b/lib/main.js
@@ -2,7 +2,7 @@ define(['moment', 'utils/router', 'leaflet', 'gui', 'helper', 'utils/language'],
   function (moment, Router, L, GUI, helper, Language) {
     'use strict';
 
-    return function (config) {
+    return function () {
       function handleData(data) {
         var timestamp;
         var nodes = [];
@@ -72,8 +72,8 @@ define(['moment', 'utils/router', 'leaflet', 'gui', 'helper', 'utils/language'],
         };
       }
 
-      var language = new Language(config);
-      var router = new Router(language);
+      var language = new Language();
+      window.router = new Router(language);
 
       config.dataPath.forEach(function (d, i) {
         config.dataPath[i] += 'meshviewer.json';
@@ -88,7 +88,7 @@ define(['moment', 'utils/router', 'leaflet', 'gui', 'helper', 'utils/language'],
 
       update()
         .then(function (d) {
-          var gui = new GUI(config, router, language);
+          var gui = new GUI(language);
           gui.setData(d);
           router.setData(d);
           router.resolve();
diff --git a/lib/map.js b/lib/map.js
index 9502ad6a3f074c5dfa7a52f9e2a0c49b56d8d18a..6b1d9fd59c4c13ad23bcdcb4567aac6bec4162fa 100644
--- a/lib/map.js
+++ b/lib/map.js
@@ -8,7 +8,7 @@ define(['map/clientlayer', 'map/labellayer', 'map/button', 'leaflet'],
       minZoom: 0
     };
 
-    return function (config, linkScale, sidebar, router, buttons) {
+    return function (linkScale, sidebar, buttons) {
       var self = this;
       var savedView;
 
@@ -57,7 +57,7 @@ define(['map/clientlayer', 'map/labellayer', 'map/button', 'leaflet'],
         baseLayers[d.name] = d.layer;
       });
 
-      var button = new Button(config, map, router, buttons);
+      var button = new Button(map, buttons);
 
       map.on('locationfound', button.locationFound);
       map.on('locationerror', button.locationError);
@@ -175,7 +175,7 @@ define(['map/clientlayer', 'map/labellayer', 'map/button', 'leaflet'],
         linkDict = {};
 
         clientLayer.setData(data);
-        labelLayer.setData(data, map, nodeDict, linkDict, linkScale, router, config);
+        labelLayer.setData(data, map, nodeDict, linkDict, linkScale);
 
         updateView(true);
       };
diff --git a/lib/map/button.js b/lib/map/button.js
index b7020ebf6a014b54baaae6207deeb47bf8756dfe..b0a2f335389bd552862883b74a6521cb040a9b49 100644
--- a/lib/map/button.js
+++ b/lib/map/button.js
@@ -63,7 +63,7 @@ define(['map/clientlayer', 'map/labellayer', 'leaflet', 'map/locationmarker'],
       }
     });
 
-    return function (config, map, router, buttons) {
+    return function (map, buttons) {
       var userLocation;
 
       var locateUserButton = new LocateButton(function (d) {
diff --git a/lib/map/labellayer.js b/lib/map/labellayer.js
index fae9c20648173501d24cd16ccdc87447904655e0..fdd4742c39fe99436d22a63a905a240ccbb03823 100644
--- a/lib/map/labellayer.js
+++ b/lib/map/labellayer.js
@@ -76,7 +76,7 @@ define(['leaflet', 'rbush', 'helper', 'moment'],
       return { minX: x, minY: y, maxX: x + width, maxY: y + height };
     }
 
-    function mkMarker(dict, iconFunc, router) {
+    function mkMarker(dict, iconFunc) {
       return function (d) {
         var m = L.circleMarker([d.location.latitude, d.location.longitude], iconFunc(d));
 
@@ -95,7 +95,7 @@ define(['leaflet', 'rbush', 'helper', 'moment'],
       };
     }
 
-    function addLinksToMap(dict, linkScale, graph, router) {
+    function addLinksToMap(dict, linkScale, graph) {
       graph = graph.filter(function (d) {
         return 'distance' in d && d.type.indexOf('vpn') !== 0;
       });
@@ -125,7 +125,7 @@ define(['leaflet', 'rbush', 'helper', 'moment'],
       });
     }
 
-    function getIcon(config, color) {
+    function getIcon(color) {
       return Object.assign({}, config.icon.base, config.icon[color]);
     }
 
@@ -136,12 +136,12 @@ define(['leaflet', 'rbush', 'helper', 'moment'],
           this.prepareLabels();
         }
       },
-      setData: function (data, map, nodeDict, linkDict, linkScale, router, config) {
-        var iconOnline = getIcon(config, 'online');
-        var iconOffline = getIcon(config, 'offline');
-        var iconLost = getIcon(config, 'lost');
-        var iconAlert = getIcon(config, 'alert');
-        var iconNew = getIcon(config, 'new');
+      setData: function (data, map, nodeDict, linkDict, linkScale) {
+        var iconOnline = getIcon('online');
+        var iconOffline = getIcon('offline');
+        var iconLost = getIcon('lost');
+        var iconAlert = getIcon('alert');
+        var iconNew = getIcon('new');
         // Check if init or data is already set
         if (groupLines) {
           groupOffline.clearLayers();
@@ -151,7 +151,7 @@ define(['leaflet', 'rbush', 'helper', 'moment'],
           groupLines.clearLayers();
         }
 
-        var lines = addLinksToMap(linkDict, linkScale, data.links, router);
+        var lines = addLinksToMap(linkDict, linkScale, data.links);
         groupLines = L.featureGroup(lines).addTo(map);
 
         var nodesOnline = helper.subtract(data.nodes.online, data.nodes.new).filter(helper.hasLocation);
@@ -161,15 +161,15 @@ define(['leaflet', 'rbush', 'helper', 'moment'],
 
         var markersOnline = nodesOnline.map(mkMarker(nodeDict, function () {
           return iconOnline;
-        }, router));
+        }));
 
         var markersOffline = nodesOffline.map(mkMarker(nodeDict, function () {
           return iconOffline;
-        }, router));
+        }));
 
         var markersNew = nodesNew.map(mkMarker(nodeDict, function () {
           return iconNew;
-        }, router));
+        }));
 
         var markersLost = nodesLost.map(mkMarker(nodeDict, function (d) {
           var age = moment(data.now).diff(d.lastseen, 'days', true);
@@ -180,7 +180,7 @@ define(['leaflet', 'rbush', 'helper', 'moment'],
             return iconLost;
           }
           return null;
-        }, router));
+        }));
 
         groupOffline = L.featureGroup(markersOffline).addTo(map);
         groupLost = L.featureGroup(markersLost).addTo(map);
diff --git a/lib/nodelist.js b/lib/nodelist.js
index f55fc8167949f689a5462b37fba34b8e22cb841c..383e15e9214bf44ad5e38dbcefe6c719f4398c5d 100644
--- a/lib/nodelist.js
+++ b/lib/nodelist.js
@@ -47,7 +47,7 @@ define(['sorttable', 'snabbdom', 'helper'], function (SortTable, V, helper) {
     reverse: true
   }];
 
-  return function (router) {
+  return function () {
     function renderRow(d) {
       var td0Content = '';
       if (helper.hasLocation(d)) {
diff --git a/lib/proportions.js b/lib/proportions.js
index f0acdf6cdfcc619139f89de91a4a64caffe51e04..44afa8e3f827b3d1c6e6773d00c34121844c852d 100644
--- a/lib/proportions.js
+++ b/lib/proportions.js
@@ -2,7 +2,7 @@ define(['d3-interpolate', 'snabbdom', 'filters/genericnode', 'helper'],
   function (d3Interpolate, V, Filter, helper) {
     'use strict';
 
-    return function (config, filterManager) {
+    return function (filterManager) {
       var self = this;
       var scale = d3Interpolate.interpolate('#770038', '#dc0067');
       V = V.default;
diff --git a/lib/simplenodelist.js b/lib/simplenodelist.js
index e7f710e1f6a12f668042c483184913be5f747432..25cb7c49f4a2e596f939f8f4c896fdd336d220b5 100644
--- a/lib/simplenodelist.js
+++ b/lib/simplenodelist.js
@@ -2,7 +2,7 @@ define(['moment', 'snabbdom', 'helper'], function (moment, V, helper) {
   'use strict';
   V = V.default;
 
-  return function (nodes, field, router, title) {
+  return function (nodes, field, title) {
     var self = this;
     var el;
     var tbody;
diff --git a/lib/title.js b/lib/title.js
index b3855a3118edf8d23b4162c1ec84efda6def1a30..b16197382fbdcc482f7d54cc8b8f71aefb8e29f8 100644
--- a/lib/title.js
+++ b/lib/title.js
@@ -1,7 +1,7 @@
 define(function () {
   'use strict';
 
-  return function (config) {
+  return function () {
     function setTitle(d) {
       var title = [config.siteName];
 
diff --git a/lib/utils/helper.js b/lib/utils/helper.js
index 8d4da14fc73c18bc87f03c403fb8b6a07c78ad38..6dd04b5c5b728d834bdb06e9e718c2ee4042cb1e 100644
--- a/lib/utils/helper.js
+++ b/lib/utils/helper.js
@@ -118,7 +118,6 @@ define({
       value
     ]);
   },
-
   showStat: function showStat(V, o, subst) {
     var content;
     subst = typeof subst !== 'undefined' ? subst : {};
diff --git a/lib/utils/language.js b/lib/utils/language.js
index 66f16850ce200bce126ad1ee4294d69c79a05bf3..1435406cd5116056297dbaffa0c6eec667869a10 100644
--- a/lib/utils/language.js
+++ b/lib/utils/language.js
@@ -1,6 +1,6 @@
 define(['polyglot', 'moment', 'helper'], function (Polyglot, moment, helper) {
   'use strict';
-  return function (config) {
+  return function () {
     var router;
 
     function languageSelect(el) {
@@ -60,6 +60,7 @@ define(['polyglot', 'moment', 'helper'], function (Polyglot, moment, helper) {
 
     function init(r) {
       router = r;
+      /** global: _ */
       window._ = new Polyglot({ locale: getLocale(router.getLang()), allowMissing: true });
       helper.getJSON('locale/' + _.locale() + '.json?' + config.cacheBreaker).then(setTranslation);
       document.querySelector('html').setAttribute('lang', _.locale());
diff --git a/lib/utils/node.js b/lib/utils/node.js
new file mode 100644
index 0000000000000000000000000000000000000000..114decb2da0e4cbb052b225da8d7c859557d25c3
--- /dev/null
+++ b/lib/utils/node.js
@@ -0,0 +1,149 @@
+define(['snabbdom', 'helper', 'moment'], function (V, helper, moment) {
+  'use strict';
+  V = V.default;
+
+  var self = {};
+
+  function showBar(v, width, warning) {
+    return V.h('span',
+      { props: { className: 'bar' + (warning ? ' warning' : '') } },
+      [
+        V.h('span',
+          {
+            style: { width: (width * 100) + '%' }
+          }),
+        V.h('label', v)
+      ]
+    );
+  }
+
+  self.showStatus = function showStatus(d) {
+    return V.h('td',
+      { props: { className: d.is_online ? 'online' : 'offline' } },
+      _.t((d.is_online ? 'node.lastOnline' : 'node.lastOffline'), {
+        time: d.lastseen.fromNow(),
+        date: d.lastseen.format('DD.MM.YYYY, H:mm:ss')
+      }));
+  };
+
+  self.showGeoURI = function showGeoURI(d) {
+    if (!helper.hasLocation(d)) {
+      return undefined;
+    }
+
+    return V.h('td',
+      V.h('a',
+        { props: { href: 'geo:' + d.location.latitude + ',' + d.location.longitude } },
+        Number(d.location.latitude.toFixed(6)) + ', ' + Number(d.location.longitude.toFixed(6))
+      )
+    );
+  };
+
+  self.showGateway = function showGateway(d) {
+    return d.is_gateway ? _.t('yes') : undefined;
+  };
+
+  self.showFirmware = function showFirmware(d) {
+    return [
+      helper.dictGet(d, ['firmware', 'release']),
+      helper.dictGet(d, ['firmware', 'base'])
+    ].filter(function (n) {
+      return n !== null;
+    }).join(' / ') || undefined;
+  };
+
+  self.showUptime = function showUptime(d) {
+    return moment.utc(d.uptime).local().fromNow(true);
+  };
+
+  self.showFirstSeen = function showFirstSeen(d) {
+    return d.firstseen.fromNow(true);
+  };
+
+  self.showLoad = function showLoad(d) {
+    if (!d.loadavg) {
+      return undefined;
+    }
+    return showBar(d.loadavg.toFixed(2), d.loadavg % 1, d.loadavg >= d.nproc);
+  };
+
+  self.showRAM = function showRAM(d) {
+    if (!d.memory_usage) {
+      return undefined;
+    }
+    return showBar(Math.round(d.memory_usage * 100) + ' %', d.memory_usage, d.memory_usage >= 0.8);
+  };
+
+  self.showSite = function showSite(d) {
+    var rt = d.site_code;
+    if (config.siteNames) {
+      config.siteNames.forEach(function (t) {
+        if (d.site_code === t.site) {
+          rt = t.name;
+        }
+      });
+    }
+    return rt;
+  };
+
+  self.showClients = function showClients(d) {
+    if (!d.is_online) {
+      return undefined;
+    }
+
+    var clients = [
+      V.h('span', [
+        d.clients > 0 ? d.clients : _.t('none'),
+        V.h('br'),
+        V.h('i', { props: { className: 'ion-people', title: _.t('node.clients') } })
+      ]),
+      V.h('span',
+        { props: { className: 'legend-24ghz' } },
+        [
+          d.clients_wifi24,
+          V.h('br'),
+          V.h('span', { props: { className: 'symbol', title: '2,4 Ghz' } })
+        ]),
+      V.h('span',
+        { props: { className: 'legend-5ghz' } },
+        [
+          d.clients_wifi5,
+          V.h('br'),
+          V.h('span', { props: { className: 'symbol', title: '5 Ghz' } })
+        ]),
+      V.h('span',
+        { props: { className: 'legend-others' } },
+        [
+          d.clients_other,
+          V.h('br'),
+          V.h('span', { props: { className: 'symbol', title: _.t('others') } })
+        ])
+    ];
+
+    return V.h('td', { props: { className: 'clients' } }, clients);
+  };
+
+  self.showIPs = function showIPs(d) {
+    var string = [];
+    var ips = d.network.addresses;
+    ips.sort();
+    ips.forEach(function (ip, i) {
+      if (i > 0) {
+        string.push(V.h('br'));
+      }
+
+      if (ip.indexOf('fe80:') !== 0) {
+        string.push(V.h('a', { props: { href: 'http://[' + ip + ']/', target: '_blank' } }, ip));
+      } else {
+        string.push(ip);
+      }
+    });
+    return V.h('td', string);
+  };
+
+  self.showAutoupdate = function showAutoupdate(d) {
+    return d.autoupdater.enabled ? _.t('node.activated', { branch: d.autoupdater.branch }) : _.t('node.deactivated');
+  };
+
+  return self;
+});