luci2: make sort / remove column in L.cbi.TableSection as narrow as possible
[project/luci2/ui.git] / luci2 / htdocs / luci2 / luci2.js
index cf1cb0a..7ade584 100644 (file)
@@ -175,7 +175,7 @@ String.prototype.format = function()
 
 function LuCI2()
 {
-       var _luci2 = this;
+       var L = this;
 
        var Class = function() { };
 
@@ -253,42 +253,42 @@ function LuCI2()
                plural:  function(n) { return 0 + (n != 1) },
 
                init: function() {
-                       if (_luci2.i18n.loaded)
+                       if (L.i18n.loaded)
                                return;
 
                        var lang = (navigator.userLanguage || navigator.language || 'en').toLowerCase();
                        var langs = (lang.indexOf('-') > -1) ? [ lang, lang.split(/-/)[0] ] : [ lang ];
 
                        for (var i = 0; i < langs.length; i++)
-                               $.ajax('%s/i18n/base.%s.json'.format(_luci2.globals.resource, langs[i]), {
+                               $.ajax('%s/i18n/base.%s.json'.format(L.globals.resource, langs[i]), {
                                        async:    false,
                                        cache:    true,
                                        dataType: 'json',
                                        success:  function(data) {
-                                               $.extend(_luci2.i18n.catalog, data);
+                                               $.extend(L.i18n.catalog, data);
 
-                                               var pe = _luci2.i18n.catalog[''];
+                                               var pe = L.i18n.catalog[''];
                                                if (pe)
                                                {
-                                                       delete _luci2.i18n.catalog[''];
+                                                       delete L.i18n.catalog[''];
                                                        try {
                                                                var pf = new Function('n', 'return 0 + (' + pe + ')');
-                                                               _luci2.i18n.plural = pf;
+                                                               L.i18n.plural = pf;
                                                        } catch (e) { };
                                                }
                                        }
                                });
 
-                       _luci2.i18n.loaded = true;
+                       L.i18n.loaded = true;
                }
 
        };
 
        this.tr = function(msgid)
        {
-               _luci2.i18n.init();
+               L.i18n.init();
 
-               var msgstr = _luci2.i18n.catalog[msgid];
+               var msgstr = L.i18n.catalog[msgid];
 
                if (typeof(msgstr) == 'undefined')
                        return msgid;
@@ -300,23 +300,23 @@ function LuCI2()
 
        this.trp = function(msgid, msgid_plural, count)
        {
-               _luci2.i18n.init();
+               L.i18n.init();
 
-               var msgstr = _luci2.i18n.catalog[msgid];
+               var msgstr = L.i18n.catalog[msgid];
 
                if (typeof(msgstr) == 'undefined')
                        return (count == 1) ? msgid : msgid_plural;
                else if (typeof(msgstr) == 'string')
                        return msgstr;
                else
-                       return msgstr[_luci2.i18n.plural(count)];
+                       return msgstr[L.i18n.plural(count)];
        };
 
        this.trc = function(msgctx, msgid)
        {
-               _luci2.i18n.init();
+               L.i18n.init();
 
-               var msgstr = _luci2.i18n.catalog[msgid + '\u0004' + msgctx];
+               var msgstr = L.i18n.catalog[msgid + '\u0004' + msgctx];
 
                if (typeof(msgstr) == 'undefined')
                        return msgid;
@@ -328,16 +328,16 @@ function LuCI2()
 
        this.trcp = function(msgctx, msgid, msgid_plural, count)
        {
-               _luci2.i18n.init();
+               L.i18n.init();
 
-               var msgstr = _luci2.i18n.catalog[msgid + '\u0004' + msgctx];
+               var msgstr = L.i18n.catalog[msgid + '\u0004' + msgctx];
 
                if (typeof(msgstr) == 'undefined')
                        return (count == 1) ? msgid : msgid_plural;
                else if (typeof(msgstr) == 'string')
                        return msgstr;
                else
-                       return msgstr[_luci2.i18n.plural(count)];
+                       return msgstr[L.i18n.plural(count)];
        };
 
        this.setHash = function(key, value)
@@ -485,6 +485,152 @@ function LuCI2()
                return n;
        };
 
+       this.toColor = function(str)
+       {
+               if (typeof(str) != 'string' || str.length == 0)
+                       return '#CCCCCC';
+
+               if (str == 'wan')
+                       return '#F09090';
+               else if (str == 'lan')
+                       return '#90F090';
+
+               var i = 0, hash = 0;
+
+               while (i < str.length)
+                       hash = str.charCodeAt(i++) + ((hash << 5) - hash);
+
+               var r = (hash & 0xFF) % 128;
+               var g = ((hash >> 8) & 0xFF) % 128;
+
+               var min = 0;
+               var max = 128;
+
+               if ((r + g) < 128)
+                       min = 128 - r - g;
+               else
+                       max = 255 - r - g;
+
+               var b = min + (((hash >> 16) & 0xFF) % (max - min));
+
+               return '#%02X%02X%02X'.format(0xFF - r, 0xFF - g, 0xFF - b);
+       };
+
+       this.parseIPv4 = function(str)
+       {
+               if ((typeof(str) != 'string' && !(str instanceof String)) ||
+                   !str.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/))
+                       return undefined;
+
+               var num = [ ];
+               var parts = str.split(/\./);
+
+               for (var i = 0; i < parts.length; i++)
+               {
+                       var n = parseInt(parts[i], 10);
+                       if (isNaN(n) || n > 255)
+                               return undefined;
+
+                       num.push(n);
+               }
+
+               return num;
+       };
+
+       this.parseIPv6 = function(str)
+       {
+               if ((typeof(str) != 'string' && !(str instanceof String)) ||
+                   !str.match(/^[a-fA-F0-9:]+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/))
+                       return undefined;
+
+               var parts = str.split(/::/);
+               if (parts.length == 0 || parts.length > 2)
+                       return undefined;
+
+               var lnum = [ ];
+               if (parts[0].length > 0)
+               {
+                       var left = parts[0].split(/:/);
+                       for (var i = 0; i < left.length; i++)
+                       {
+                               var n = parseInt(left[i], 16);
+                               if (isNaN(n))
+                                       return undefined;
+
+                               lnum.push((n / 256) >> 0);
+                               lnum.push(n % 256);
+                       }
+               }
+
+               var rnum = [ ];
+               if (parts.length > 1 && parts[1].length > 0)
+               {
+                       var right = parts[1].split(/:/);
+
+                       for (var i = 0; i < right.length; i++)
+                       {
+                               if (right[i].indexOf('.') > 0)
+                               {
+                                       var addr = L.parseIPv4(right[i]);
+                                       if (!addr)
+                                               return undefined;
+
+                                       rnum.push.apply(rnum, addr);
+                                       continue;
+                               }
+
+                               var n = parseInt(right[i], 16);
+                               if (isNaN(n))
+                                       return undefined;
+
+                               rnum.push((n / 256) >> 0);
+                               rnum.push(n % 256);
+                       }
+               }
+
+               if (rnum.length > 0 && (lnum.length + rnum.length) > 15)
+                       return undefined;
+
+               var num = [ ];
+
+               num.push.apply(num, lnum);
+
+               for (var i = 0; i < (16 - lnum.length - rnum.length); i++)
+                       num.push(0);
+
+               num.push.apply(num, rnum);
+
+               if (num.length > 16)
+                       return undefined;
+
+               return num;
+       };
+
+       this.isNetmask = function(addr)
+       {
+               if (!$.isArray(addr))
+                       return false;
+
+               var c;
+
+               for (c = 0; (c < addr.length) && (addr[c] == 255); c++);
+
+               if (c == addr.length)
+                       return true;
+
+               if ((addr[c] == 254) || (addr[c] == 252) || (addr[c] == 248) ||
+                       (addr[c] == 240) || (addr[c] == 224) || (addr[c] == 192) ||
+                       (addr[c] == 128) || (addr[c] == 0))
+               {
+                       for (c++; (c < addr.length) && (addr[c] == 0); c++);
+
+                       if (c == addr.length)
+                               return true;
+               }
+
+               return false;
+       };
+
        this.globals = {
                timeout:  15000,
                resource: '/luci2',
@@ -505,7 +651,7 @@ function LuCI2()
                                data:        JSON.stringify(req),
                                dataType:    'json',
                                type:        'POST',
-                               timeout:     _luci2.globals.timeout,
+                               timeout:     L.globals.timeout,
                                _rpc_req:   req
                        }).then(cb, cb);
                },
@@ -536,7 +682,7 @@ function LuCI2()
                        for (var i = 0; i < msg.length; i++)
                        {
                                /* fetch related request info */
-                               var req = _luci2.rpc._requests[reqs[i].id];
+                               var req = L.rpc._requests[reqs[i].id];
                                if (typeof(req) != 'object')
                                        throw 'No related request for JSON response';
 
@@ -567,7 +713,7 @@ function LuCI2()
                                {
                                        req.priv[0] = ret;
                                        req.priv[1] = req.params;
-                                       ret = req.filter.apply(_luci2.rpc, req.priv);
+                                       ret = req.filter.apply(L.rpc, req.priv);
                                }
 
                                /* store response data */
@@ -577,7 +723,7 @@ function LuCI2()
                                        data = ret;
 
                                /* delete request object */
-                               delete _luci2.rpc._requests[reqs[i].id];
+                               delete L.rpc._requests[reqs[i].id];
                        }
 
                        return $.Deferred().resolveWith(this, [ data ]);
@@ -608,7 +754,7 @@ function LuCI2()
                flush: function()
                {
                        if (!$.isArray(this._batch))
-                               return _luci2.deferrable([ ]);
+                               return L.deferrable([ ]);
 
                        var req = this._batch;
                        delete this._batch;
@@ -648,7 +794,7 @@ function LuCI2()
                                        id:      _rpc._id++,
                                        method:  'call',
                                        params:  [
-                                               _luci2.globals.sid,
+                                               L.globals.sid,
                                                options.object,
                                                options.method,
                                                params
@@ -660,7 +806,7 @@ function LuCI2()
                                if ($.isArray(_rpc._batch))
                                {
                                        req.index = _rpc._batch.push(msg) - 1;
-                                       return _luci2.deferrable(msg);
+                                       return L.deferrable(msg);
                                }
 
                                /* call rpc */
@@ -674,7 +820,7 @@ function LuCI2()
                init: function()
                {
                        this.state = {
-                               newid  0,
+                               newidx:  0,
                                values:  { },
                                creates: { },
                                changes: { },
@@ -683,38 +829,51 @@ function LuCI2()
                        };
                },
 
-               _load: _luci2.rpc.declare({
+               _load: L.rpc.declare({
                        object: 'uci',
                        method: 'get',
                        params: [ 'config' ],
                        expect: { values: { } }
                }),
 
-               _order: _luci2.rpc.declare({
+               _order: L.rpc.declare({
                        object: 'uci',
                        method: 'order',
                        params: [ 'config', 'sections' ]
                }),
 
-               _add: _luci2.rpc.declare({
+               _add: L.rpc.declare({
                        object: 'uci',
                        method: 'add',
                        params: [ 'config', 'type', 'name', 'values' ],
                        expect: { section: '' }
                }),
 
-               _set: _luci2.rpc.declare({
+               _set: L.rpc.declare({
                        object: 'uci',
                        method: 'set',
                        params: [ 'config', 'section', 'values' ]
                }),
 
-               _delete: _luci2.rpc.declare({
+               _delete: L.rpc.declare({
                        object: 'uci',
                        method: 'delete',
                        params: [ 'config', 'section', 'options' ]
                }),
 
+               _newid: function(conf)
+               {
+                       var v = this.state.values;
+                       var n = this.state.creates;
+                       var sid;
+
+                       do {
+                               sid = "new%06x".format(Math.random() * 0xFFFFFF);
+                       } while ((n[conf] && n[conf][sid]) || (v[conf] && v[conf][sid]));
+
+                       return sid;
+               },
+
                load: function(packages)
                {
                        var self = this;
@@ -724,17 +883,17 @@ function LuCI2()
                        if (!$.isArray(packages))
                                packages = [ packages ];
 
-                       _luci2.rpc.batch();
+                       L.rpc.batch();
 
                        for (var i = 0; i < packages.length; i++)
-                               if (!seen[packages[i]])
+                               if (!seen[packages[i]] && !self.state.values[packages[i]])
                                {
                                        pkgs.push(packages[i]);
                                        seen[packages[i]] = true;
                                        self._load(packages[i]);
                                }
 
-                       return _luci2.rpc.flush().then(function(responses) {
+                       return L.rpc.flush().then(function(responses) {
                                for (var i = 0; i < responses.length; i++)
                                        self.state.values[pkgs[i]] = responses[i];
 
@@ -758,21 +917,21 @@ function LuCI2()
 
                add: function(conf, type, name)
                {
-                       var c = this.state.creates;
-                       var s = '.new.%d'.format(this.state.newid++);
+                       var n = this.state.creates;
+                       var sid = this._newid(conf);
 
-                       if (!c[conf])
-                               c[conf] = { };
+                       if (!n[conf])
+                               n[conf] = { };
 
-                       c[conf][s] = {
+                       n[conf][sid] = {
                                '.type':      type,
-                               '.name':      s,
+                               '.name':      sid,
                                '.create':    name,
                                '.anonymous': !name,
-                               '.index':     1000 + this.state.newid
+                               '.index':     1000 + this.state.newidx++
                        };
 
-                       return s;
+                       return sid;
                },
 
                remove: function(conf, sid)
@@ -782,10 +941,9 @@ function LuCI2()
                        var d = this.state.deletes;
 
                        /* requested deletion of a just created section */
-                       if (sid.indexOf('.new.') == 0)
+                       if (n[conf] && n[conf][sid])
                        {
-                               if (n[conf])
-                                       delete n[conf][sid];
+                               delete n[conf][sid];
                        }
                        else
                        {
@@ -845,7 +1003,7 @@ function LuCI2()
                                return undefined;
 
                        /* requested option in a just created section */
-                       if (sid.indexOf('.new.') == 0)
+                       if (n[conf] && n[conf][sid])
                        {
                                if (!n[conf])
                                        return undefined;
@@ -890,6 +1048,7 @@ function LuCI2()
 
                set: function(conf, sid, opt, val)
                {
+                       var v = this.state.values;
                        var n = this.state.creates;
                        var c = this.state.changes;
                        var d = this.state.deletes;
@@ -899,15 +1058,12 @@ function LuCI2()
                            opt.charAt(0) == '.')
                                return;
 
-                       if (sid.indexOf('.new.') == 0)
+                       if (n[conf] && n[conf][sid])
                        {
-                               if (n[conf] && n[conf][sid])
-                               {
-                                       if (typeof(val) != 'undefined')
-                                               n[conf][sid][opt] = val;
-                                       else
-                                               delete n[conf][sid][opt];
-                               }
+                               if (typeof(val) != 'undefined')
+                                       n[conf][sid][opt] = val;
+                               else
+                                       delete n[conf][sid][opt];
                        }
                        else if (typeof(val) != 'undefined')
                        {
@@ -915,6 +1071,10 @@ function LuCI2()
                                if (d[conf] && d[conf][sid] === true)
                                        return;
 
+                               /* only set in existing sections */
+                               if (!v[conf] || !v[conf][sid])
+                                       return;
+
                                if (!c[conf])
                                        c[conf] = { };
 
@@ -923,12 +1083,16 @@ function LuCI2()
 
                                /* undelete option */
                                if (d[conf] && d[conf][sid])
-                                       d[conf][sid] = _luci2.filterArray(d[conf][sid], opt);
+                                       d[conf][sid] = L.filterArray(d[conf][sid], opt);
 
                                c[conf][sid][opt] = val;
                        }
                        else
                        {
+                               /* only delete in existing sections */
+                               if (!v[conf] || !v[conf][sid])
+                                       return;
+
                                if (!d[conf])
                                        d[conf] = { };
 
@@ -945,6 +1109,35 @@ function LuCI2()
                        return this.set(conf, sid, opt, undefined);
                },
 
+               get_first: function(conf, type, opt)
+               {
+                       var sid = undefined;
+
+                       L.uci.sections(conf, type, function(s) {
+                               if (typeof(sid) != 'string')
+                                       sid = s['.name'];
+                       });
+
+                       return this.get(conf, sid, opt);
+               },
+
+               set_first: function(conf, type, opt, val)
+               {
+                       var sid = undefined;
+
+                       L.uci.sections(conf, type, function(s) {
+                               if (typeof(sid) != 'string')
+                                       sid = s['.name'];
+                       });
+
+                       return this.set(conf, sid, opt, val);
+               },
+
+               unset_first: function(conf, type, opt)
+               {
+                       return this.set_first(conf, type, opt, undefined);
+               },
+
                _reload: function()
                {
                        var pkgs = [ ];
@@ -964,9 +1157,9 @@ function LuCI2()
                        var r = this.state.reorder;
 
                        if ($.isEmptyObject(r))
-                               return _luci2.deferrable();
+                               return L.deferrable();
 
-                       _luci2.rpc.batch();
+                       L.rpc.batch();
 
                        /*
                         gather all created and existing sections, sort them according
@@ -976,7 +1169,7 @@ function LuCI2()
                        {
                                var o = [ ];
 
-                               if (n && n[c])
+                               if (n[c])
                                        for (var s in n[c])
                                                o.push(n[c][s]);
 
@@ -999,7 +1192,7 @@ function LuCI2()
                        }
 
                        this.state.reorder = { };
-                       return _luci2.rpc.flush();
+                       return L.rpc.flush();
                },
 
                swap: function(conf, sid1, sid2)
@@ -1022,49 +1215,67 @@ function LuCI2()
 
                save: function()
                {
-                       _luci2.rpc.batch();
+                       L.rpc.batch();
+
+                       var v = this.state.values;
+                       var n = this.state.creates;
+                       var c = this.state.changes;
+                       var d = this.state.deletes;
 
                        var self = this;
                        var snew = [ ];
+                       var pkgs = { };
 
-                       if (self.state.creates)
-                               for (var c in self.state.creates)
-                                       for (var s in self.state.creates[c])
+                       if (n)
+                               for (var conf in n)
+                               {
+                                       for (var sid in n[conf])
                                        {
                                                var r = {
-                                                       config: c,
+                                                       config: conf,
                                                        values: { }
                                                };
 
-                                               for (var k in self.state.creates[c][s])
+                                               for (var k in n[conf][sid])
                                                {
                                                        if (k == '.type')
-                                                               r.type = self.state.creates[c][s][k];
+                                                               r.type = n[conf][sid][k];
                                                        else if (k == '.create')
-                                                               r.name = self.state.creates[c][s][k];
+                                                               r.name = n[conf][sid][k];
                                                        else if (k.charAt(0) != '.')
-                                                               r.values[k] = self.state.creates[c][s][k];
+                                                               r.values[k] = n[conf][sid][k];
                                                }
 
-                                               snew.push(self.state.creates[c][s]);
+                                               snew.push(n[conf][sid]);
 
                                                self._add(r.config, r.type, r.name, r.values);
                                        }
 
-                       if (self.state.changes)
-                               for (var c in self.state.changes)
-                                       for (var s in self.state.changes[c])
-                                               self._set(c, s, self.state.changes[c][s]);
+                                       pkgs[conf] = true;
+                               }
+
+                       if (c)
+                               for (var conf in c)
+                               {
+                                       for (var sid in c[conf])
+                                               self._set(conf, sid, c[conf][sid]);
+
+                                       pkgs[conf] = true;
+                               }
 
-                       if (self.state.deletes)
-                               for (var c in self.state.deletes)
-                                       for (var s in self.state.deletes[c])
+                       if (d)
+                               for (var conf in d)
+                               {
+                                       for (var sid in d[conf])
                                        {
-                                               var o = self.state.deletes[c][s];
-                                               self._delete(c, s, (o === true) ? undefined : o);
+                                               var o = d[conf][sid];
+                                               self._delete(conf, sid, (o === true) ? undefined : o);
                                        }
 
-                       return _luci2.rpc.flush().then(function(responses) {
+                                       pkgs[conf] = true;
+                               }
+
+                       return L.rpc.flush().then(function(responses) {
                                /*
                                 array "snew" holds references to the created uci sections,
                                 use it to assign the returned names of the new sections
@@ -1073,16 +1284,22 @@ function LuCI2()
                                        snew[i]['.name'] = responses[i];
 
                                return self._reorder();
+                       }).then(function() {
+                               pkgs = L.toArray(pkgs);
+
+                               self.unload(pkgs);
+
+                               return self.load(pkgs);
                        });
                },
 
-               _apply: _luci2.rpc.declare({
+               _apply: L.rpc.declare({
                        object: 'uci',
                        method: 'apply',
                        params: [ 'timeout', 'rollback' ]
                }),
 
-               _confirm: _luci2.rpc.declare({
+               _confirm: L.rpc.declare({
                        object: 'uci',
                        method: 'confirm'
                }),
@@ -1127,7 +1344,7 @@ function LuCI2()
                        return deferred;
                },
 
-               changes: _luci2.rpc.declare({
+               changes: L.rpc.declare({
                        object: 'uci',
                        method: 'changes',
                        expect: { changes: { } }
@@ -1135,19 +1352,19 @@ function LuCI2()
 
                readable: function(conf)
                {
-                       return _luci2.session.hasACL('uci', conf, 'read');
+                       return L.session.hasACL('uci', conf, 'read');
                },
 
                writable: function(conf)
                {
-                       return _luci2.session.hasACL('uci', conf, 'write');
+                       return L.session.hasACL('uci', conf, 'write');
                }
        });
 
        this.uci = new this.UCIContext();
 
        this.wireless = {
-               listDeviceNames: _luci2.rpc.declare({
+               listDeviceNames: L.rpc.declare({
                        object: 'iwinfo',
                        method: 'devices',
                        expect: { 'devices': [ ] },
@@ -1157,7 +1374,7 @@ function LuCI2()
                        }
                }),
 
-               getDeviceStatus: _luci2.rpc.declare({
+               getDeviceStatus: L.rpc.declare({
                        object: 'iwinfo',
                        method: 'info',
                        params: [ 'device' ],
@@ -1172,7 +1389,7 @@ function LuCI2()
                        }
                }),
 
-               getAssocList: _luci2.rpc.declare({
+               getAssocList: L.rpc.declare({
                        object: 'iwinfo',
                        method: 'assoclist',
                        params: [ 'device' ],
@@ -1196,12 +1413,12 @@ function LuCI2()
 
                getWirelessStatus: function() {
                        return this.listDeviceNames().then(function(names) {
-                               _luci2.rpc.batch();
+                               L.rpc.batch();
 
                                for (var i = 0; i < names.length; i++)
-                                       _luci2.wireless.getDeviceStatus(names[i]);
+                                       L.wireless.getDeviceStatus(names[i]);
 
-                               return _luci2.rpc.flush();
+                               return L.rpc.flush();
                        }).then(function(networks) {
                                var rv = { };
 
@@ -1241,12 +1458,12 @@ function LuCI2()
                getAssocLists: function()
                {
                        return this.listDeviceNames().then(function(names) {
-                               _luci2.rpc.batch();
+                               L.rpc.batch();
 
                                for (var i = 0; i < names.length; i++)
-                                       _luci2.wireless.getAssocList(names[i]);
+                                       L.wireless.getAssocList(names[i]);
 
-                               return _luci2.rpc.flush();
+                               return L.rpc.flush();
                        }).then(function(assoclists) {
                                var rv = [ ];
 
@@ -1269,21 +1486,21 @@ function LuCI2()
                        }
 
                        if (!enc || !enc.enabled)
-                               return _luci2.tr('None');
+                               return L.tr('None');
 
                        if (enc.wep)
                        {
                                if (enc.wep.length == 2)
-                                       return _luci2.tr('WEP Open/Shared') + ' (%s)'.format(format_list(enc.ciphers, ', '));
+                                       return L.tr('WEP Open/Shared') + ' (%s)'.format(format_list(enc.ciphers, ', '));
                                else if (enc.wep[0] == 'shared')
-                                       return _luci2.tr('WEP Shared Auth') + ' (%s)'.format(format_list(enc.ciphers, ', '));
+                                       return L.tr('WEP Shared Auth') + ' (%s)'.format(format_list(enc.ciphers, ', '));
                                else
-                                       return _luci2.tr('WEP Open System') + ' (%s)'.format(format_list(enc.ciphers, ', '));
+                                       return L.tr('WEP Open System') + ' (%s)'.format(format_list(enc.ciphers, ', '));
                        }
                        else if (enc.wpa)
                        {
                                if (enc.wpa.length == 2)
-                                       return _luci2.tr('mixed WPA/WPA2') + ' %s (%s)'.format(
+                                       return L.tr('mixed WPA/WPA2') + ' %s (%s)'.format(
                                                format_list(enc.authentication, '/'),
                                                format_list(enc.ciphers, ', ')
                                        );
@@ -1299,7 +1516,7 @@ function LuCI2()
                                        );
                        }
 
-                       return _luci2.tr('Unknown');
+                       return L.tr('Unknown');
                }
        };
 
@@ -1330,7 +1547,7 @@ function LuCI2()
                        var self = this;
                        var zone = undefined;
 
-                       return _luci2.uci.sections('firewall', 'zone', function(z) {
+                       return L.uci.sections('firewall', 'zone', function(z) {
                                if (!z.name || !z.network)
                                        return;
 
@@ -1365,37 +1582,37 @@ function LuCI2()
                ],
 
                _cache_functions: [
-                       'protolist', 0, _luci2.rpc.declare({
+                       'protolist', 0, L.rpc.declare({
                                object: 'network',
                                method: 'get_proto_handlers',
                                expect: { '': { } }
                        }),
-                       'ifstate', 1, _luci2.rpc.declare({
+                       'ifstate', 1, L.rpc.declare({
                                object: 'network.interface',
                                method: 'dump',
                                expect: { 'interface': [ ] }
                        }),
-                       'devstate', 2, _luci2.rpc.declare({
+                       'devstate', 2, L.rpc.declare({
                                object: 'network.device',
                                method: 'status',
                                expect: { '': { } }
                        }),
-                       'wifistate', 0, _luci2.rpc.declare({
+                       'wifistate', 0, L.rpc.declare({
                                object: 'network.wireless',
                                method: 'status',
                                expect: { '': { } }
                        }),
-                       'bwstate', 2, _luci2.rpc.declare({
+                       'bwstate', 2, L.rpc.declare({
                                object: 'luci2.network.bwmon',
                                method: 'statistics',
                                expect: { 'statistics': { } }
                        }),
-                       'devlist', 2, _luci2.rpc.declare({
+                       'devlist', 2, L.rpc.declare({
                                object: 'luci2.network',
                                method: 'device_list',
                                expect: { 'devices': [ ] }
                        }),
-                       'swlist', 0, _luci2.rpc.declare({
+                       'swlist', 0, L.rpc.declare({
                                object: 'luci2.network',
                                method: 'switch_list',
                                expect: { 'switches': [ ] }
@@ -1404,8 +1621,8 @@ function LuCI2()
 
                _fetch_protocol: function(proto)
                {
-                       var url = _luci2.globals.resource + '/proto/' + proto + '.js';
-                       var self = _luci2.NetworkModel;
+                       var url = L.globals.resource + '/proto/' + proto + '.js';
+                       var self = L.NetworkModel;
 
                        var def = $.Deferred();
 
@@ -1418,7 +1635,7 @@ function LuCI2()
                                        var protoConstructorSource = (
                                                '(function(L, $) { ' +
                                                        'return %s' +
-                                               '})(_luci2, $);\n\n' +
+                                               '})(L, $);\n\n' +
                                                '//@ sourceURL=%s'
                                        ).format(data, url);
 
@@ -1440,8 +1657,10 @@ function LuCI2()
 
                _fetch_protocols: function()
                {
-                       var self = _luci2.NetworkModel;
-                       var deferreds = [ ];
+                       var self = L.NetworkModel;
+                       var deferreds = [
+                               self._fetch_protocol('none')
+                       ];
 
                        for (var proto in self._cache.protolist)
                                deferreds.push(self._fetch_protocol(proto));
@@ -1449,7 +1668,7 @@ function LuCI2()
                        return $.when.apply($, deferreds);
                },
 
-               _fetch_swstate: _luci2.rpc.declare({
+               _fetch_swstate: L.rpc.declare({
                        object: 'luci2.network',
                        method: 'switch_info',
                        params: [ 'switch' ],
@@ -1457,7 +1676,7 @@ function LuCI2()
                }),
 
                _fetch_swstate_cb: function(responses) {
-                       var self = _luci2.NetworkModel;
+                       var self = L.NetworkModel;
                        var swlist = self._cache.swlist;
                        var swstate = self._cache.swstate = { };
 
@@ -1467,7 +1686,7 @@ function LuCI2()
 
                _fetch_cache_cb: function(level)
                {
-                       var self = _luci2.NetworkModel;
+                       var self = L.NetworkModel;
                        var name = '_fetch_cache_cb_' + level;
 
                        return self[name] || (
@@ -1479,42 +1698,42 @@ function LuCI2()
 
                                        if (!level)
                                        {
-                                               _luci2.rpc.batch();
+                                               L.rpc.batch();
 
                                                for (var i = 0; i < self._cache.swlist.length; i++)
                                                        self._fetch_swstate(self._cache.swlist[i]);
 
-                                               return _luci2.rpc.flush().then(self._fetch_swstate_cb);
+                                               return L.rpc.flush().then(self._fetch_swstate_cb);
                                        }
 
-                                       return _luci2.deferrable();
+                                       return L.deferrable();
                                }
                        );
                },
 
                _fetch_cache: function(level)
                {
-                       var self = _luci2.NetworkModel;
+                       var self = L.NetworkModel;
 
-                       return _luci2.uci.load(['network', 'wireless']).then(function() {
-                               _luci2.rpc.batch();
+                       return L.uci.load(['network', 'wireless']).then(function() {
+                               L.rpc.batch();
 
                                for (var i = 0; i < self._cache_functions.length; i += 3)
                                        if (!level || self._cache_functions[i + 1] == level)
                                                self._cache_functions[i + 2]();
 
-                               return _luci2.rpc.flush().then(self._fetch_cache_cb(level || 0));
+                               return L.rpc.flush().then(self._fetch_cache_cb(level || 0));
                        });
                },
 
                _get: function(pkg, sid, key)
                {
-                       return _luci2.uci.get(pkg, sid, key);
+                       return L.uci.get(pkg, sid, key);
                },
 
                _set: function(pkg, sid, key, val)
                {
-                       return _luci2.uci.set(pkg, sid, key, val);
+                       return L.uci.set(pkg, sid, key, val);
                },
 
                _is_blacklisted: function(dev)
@@ -1568,7 +1787,7 @@ function LuCI2()
 
                _parse_devices: function()
                {
-                       var self = _luci2.NetworkModel;
+                       var self = L.NetworkModel;
                        var wificount = { };
 
                        for (var ifname in self._cache.devstate)
@@ -1627,7 +1846,7 @@ function LuCI2()
                                }
                        }
 
-                       var net = _luci2.uci.sections('network');
+                       var net = L.uci.sections('network');
                        for (var i = 0; i < net.length; i++)
                        {
                                var s = net[i];
@@ -1649,7 +1868,7 @@ function LuCI2()
                                }
                                else if (s['.type'] == 'interface' && !s['.anonymous'] && s.ifname)
                                {
-                                       var ifnames = _luci2.toArray(s.ifname);
+                                       var ifnames = L.toArray(s.ifname);
 
                                        for (var j = 0; j < ifnames.length; j++)
                                                self._get_dev(ifnames[j]);
@@ -1667,7 +1886,7 @@ function LuCI2()
                                {
                                        var sw = self._cache.swstate[s.device];
                                        var vid = parseInt(s.vid || s.vlan);
-                                       var ports = _luci2.toArray(s.ports);
+                                       var ports = L.toArray(s.ports);
 
                                        if (!sw || !ports.length || isNaN(vid))
                                                continue;
@@ -1703,7 +1922,7 @@ function LuCI2()
                                }
                        }
 
-                       var wifi = _luci2.uci.sections('wireless');
+                       var wifi = L.uci.sections('wireless');
                        for (var i = 0; i < wifi.length; i++)
                        {
                                var s = wifi[i];
@@ -1748,7 +1967,7 @@ function LuCI2()
 
                                if (s['.type'] == 'interface' && !s['.anonymous'] && s.type == 'bridge')
                                {
-                                       var ifnames = _luci2.toArray(s.ifname);
+                                       var ifnames = L.toArray(s.ifname);
 
                                        for (var ifname in self._devs)
                                        {
@@ -1757,7 +1976,7 @@ function LuCI2()
                                                if (dev.kind != 'wifi')
                                                        continue;
 
-                                               var wnets = _luci2.toArray(_luci2.uci.get('wireless', dev.sid, 'network'));
+                                               var wnets = L.toArray(L.uci.get('wireless', dev.sid, 'network'));
                                                if ($.inArray(sid, wnets) > -1)
                                                        ifnames.push(ifname);
                                        }
@@ -1773,8 +1992,8 @@ function LuCI2()
 
                _parse_interfaces: function()
                {
-                       var self = _luci2.NetworkModel;
-                       var net = _luci2.uci.sections('network');
+                       var self = L.NetworkModel;
+                       var net = L.uci.sections('network');
 
                        for (var i = 0; i < net.length; i++)
                        {
@@ -1789,7 +2008,7 @@ function LuCI2()
                                        var l3dev = undefined;
                                        var l2dev = undefined;
 
-                                       var ifnames = _luci2.toArray(s.ifname);
+                                       var ifnames = L.toArray(s.ifname);
 
                                        for (var ifname in self._devs)
                                        {
@@ -1798,7 +2017,7 @@ function LuCI2()
                                                if (dev.kind != 'wifi')
                                                        continue;
 
-                                               var wnets = _luci2.toArray(_luci2.uci.get('wireless', dev.sid, 'network'));
+                                               var wnets = L.toArray(L.uci.get('wireless', dev.sid, 'network'));
                                                if ($.inArray(entry.name, wnets) > -1)
                                                        ifnames.push(ifname);
                                        }
@@ -1844,7 +2063,7 @@ function LuCI2()
                        var self = this;
 
                        if (self._cache)
-                               return _luci2.deferrable();
+                               return L.deferrable();
 
                        self._cache  = { };
                        self._devs   = { };
@@ -1887,14 +2106,14 @@ function LuCI2()
 
                        for (var ifname in this._devs)
                                if (ifname != 'lo')
-                                       devs.push(new _luci2.NetworkModel.Device(this._devs[ifname]));
+                                       devs.push(new L.NetworkModel.Device(this._devs[ifname]));
 
                        return devs.sort(this._sort_devices);
                },
 
                getDeviceByInterface: function(iface)
                {
-                       if (iface instanceof _luci2.NetworkModel.Interface)
+                       if (iface instanceof L.NetworkModel.Interface)
                                iface = iface.name();
 
                        if (this._ifaces[iface])
@@ -1907,14 +2126,14 @@ function LuCI2()
                getDevice: function(ifname)
                {
                        if (this._devs[ifname])
-                               return new _luci2.NetworkModel.Device(this._devs[ifname]);
+                               return new L.NetworkModel.Device(this._devs[ifname]);
 
                        return undefined;
                },
 
                createDevice: function(name)
                {
-                       return new _luci2.NetworkModel.Device(this._get_dev(name));
+                       return new L.NetworkModel.Device(this._get_dev(name));
                },
 
                getInterfaces: function()
@@ -1941,7 +2160,7 @@ function LuCI2()
                {
                        var ifaces = [ ];
 
-                       if (dev instanceof _luci2.NetworkModel.Device)
+                       if (dev instanceof L.NetworkModel.Device)
                                dev = dev.name();
 
                        for (var name in this._ifaces)
@@ -1966,7 +2185,7 @@ function LuCI2()
                getInterface: function(iface)
                {
                        if (this._ifaces[iface])
-                               return new _luci2.NetworkModel.Interface(this._ifaces[iface]);
+                               return new L.NetworkModel.Interface(this._ifaces[iface]);
 
                        return undefined;
                },
@@ -2030,7 +2249,7 @@ function LuCI2()
 
                resolveAlias: function(ifname)
                {
-                       if (ifname instanceof _luci2.NetworkModel.Device)
+                       if (ifname instanceof L.NetworkModel.Device)
                                ifname = ifname.name();
 
                        var dev = this._devs[ifname];
@@ -2054,16 +2273,16 @@ function LuCI2()
 
        this.NetworkModel.Device = Class.extend({
                _wifi_modes: {
-                       ap: _luci2.tr('Master'),
-                       sta: _luci2.tr('Client'),
-                       adhoc: _luci2.tr('Ad-Hoc'),
-                       monitor: _luci2.tr('Monitor'),
-                       wds: _luci2.tr('Static WDS')
+                       ap: L.tr('Master'),
+                       sta: L.tr('Client'),
+                       adhoc: L.tr('Ad-Hoc'),
+                       monitor: L.tr('Monitor'),
+                       wds: L.tr('Static WDS')
                },
 
                _status: function(key)
                {
-                       var s = _luci2.NetworkModel._cache.devstate[this.options.ifname];
+                       var s = L.NetworkModel._cache.devstate[this.options.ifname];
 
                        if (s)
                                return key ? s[key] : s;
@@ -2075,14 +2294,14 @@ function LuCI2()
                {
                        var sid = this.options.sid;
                        var pkg = (this.options.kind == 'wifi') ? 'wireless' : 'network';
-                       return _luci2.NetworkModel._get(pkg, sid, key);
+                       return L.NetworkModel._get(pkg, sid, key);
                },
 
                set: function(key, val)
                {
                        var sid = this.options.sid;
                        var pkg = (this.options.kind == 'wifi') ? 'wireless' : 'network';
-                       return _luci2.NetworkModel._set(pkg, sid, key, val);
+                       return L.NetworkModel._set(pkg, sid, key, val);
                },
 
                init: function()
@@ -2107,52 +2326,52 @@ function LuCI2()
                        switch (this.options.kind)
                        {
                        case 'alias':
-                               return _luci2.tr('Alias for network "%s"').format(this.options.ifname.substring(1));
+                               return L.tr('Alias for network "%s"').format(this.options.ifname.substring(1));
 
                        case 'bridge':
-                               return _luci2.tr('Network bridge');
+                               return L.tr('Network bridge');
 
                        case 'ethernet':
-                               return _luci2.tr('Network device');
+                               return L.tr('Network device');
 
                        case 'tunnel':
                                switch (this.options.type)
                                {
                                case 1: /* tuntap */
-                                       return _luci2.tr('TAP device');
+                                       return L.tr('TAP device');
 
                                case 512: /* PPP */
-                                       return _luci2.tr('PPP tunnel');
+                                       return L.tr('PPP tunnel');
 
                                case 768: /* IP-IP Tunnel */
-                                       return _luci2.tr('IP-in-IP tunnel');
+                                       return L.tr('IP-in-IP tunnel');
 
                                case 769: /* IP6-IP6 Tunnel */
-                                       return _luci2.tr('IPv6-in-IPv6 tunnel');
+                                       return L.tr('IPv6-in-IPv6 tunnel');
 
                                case 776: /* IPv6-in-IPv4 */
-                                       return _luci2.tr('IPv6-over-IPv4 tunnel');
+                                       return L.tr('IPv6-over-IPv4 tunnel');
                                        break;
 
                                case 778: /* GRE over IP */
-                                       return _luci2.tr('GRE-over-IP tunnel');
+                                       return L.tr('GRE-over-IP tunnel');
 
                                default:
-                                       return _luci2.tr('Tunnel device');
+                                       return L.tr('Tunnel device');
                                }
 
                        case 'vlan':
-                               return _luci2.tr('VLAN %d on %s').format(this.options.vid, this.options.vsw.model);
+                               return L.tr('VLAN %d on %s').format(this.options.vid, this.options.vsw.model);
 
                        case 'wifi':
                                var o = this.options;
-                               return _luci2.trc('(Wifi-Mode) "(SSID)" on (radioX)', '%s "%h" on %s').format(
-                                       o.wmode ? this._wifi_modes[o.wmode] : _luci2.tr('Unknown mode'),
+                               return L.trc('(Wifi-Mode) "(SSID)" on (radioX)', '%s "%h" on %s').format(
+                                       o.wmode ? this._wifi_modes[o.wmode] : L.tr('Unknown mode'),
                                        o.wssid || '?', o.wdev
                                );
                        }
 
-                       return _luci2.tr('Unknown device');
+                       return L.tr('Unknown device');
                },
 
                icon: function(up)
@@ -2165,12 +2384,12 @@ function LuCI2()
                        if (typeof(up) == 'undefined')
                                up = this.isUp();
 
-                       return _luci2.globals.resource + '/icons/%s%s.png'.format(kind, up ? '' : '_disabled');
+                       return L.globals.resource + '/icons/%s%s.png'.format(kind, up ? '' : '_disabled');
                },
 
                isUp: function()
                {
-                       var l = _luci2.NetworkModel._cache.devlist;
+                       var l = L.NetworkModel._cache.devlist;
 
                        for (var i = 0; i < l.length; i++)
                                if (l[i].device == this.options.ifname)
@@ -2201,8 +2420,8 @@ function LuCI2()
 
                isInNetwork: function(net)
                {
-                       if (!(net instanceof _luci2.NetworkModel.Interface))
-                               net = _luci2.NetworkModel.getInterface(net);
+                       if (!(net instanceof L.NetworkModel.Interface))
+                               net = L.NetworkModel.getInterface(net);
 
                        if (net)
                        {
@@ -2210,7 +2429,7 @@ function LuCI2()
                                    net.options.l2dev == this.options.ifname)
                                        return true;
 
-                               var dev = _luci2.NetworkModel._devs[net.options.l2dev];
+                               var dev = L.NetworkModel._devs[net.options.l2dev];
                                if (dev && dev.kind == 'bridge' && dev.ports)
                                        return ($.inArray(this.options.ifname, dev.ports) > -1);
                        }
@@ -2220,7 +2439,7 @@ function LuCI2()
 
                getMTU: function()
                {
-                       var dev = _luci2.NetworkModel._cache.devstate[this.options.ifname];
+                       var dev = L.NetworkModel._cache.devstate[this.options.ifname];
                        if (dev && !isNaN(dev.mtu))
                                return dev.mtu;
 
@@ -2232,7 +2451,7 @@ function LuCI2()
                        if (this.options.type != 1)
                                return undefined;
 
-                       var dev = _luci2.NetworkModel._cache.devstate[this.options.ifname];
+                       var dev = L.NetworkModel._cache.devstate[this.options.ifname];
                        if (dev && dev.macaddr)
                                return dev.macaddr.toUpperCase();
 
@@ -2241,7 +2460,7 @@ function LuCI2()
 
                getInterfaces: function()
                {
-                       return _luci2.NetworkModel.getInterfacesByDevice(this.options.name);
+                       return L.NetworkModel.getInterfacesByDevice(this.options.name);
                },
 
                getStatistics: function()
@@ -2262,7 +2481,7 @@ function LuCI2()
                        for (var i = 0; i < 120; i++)
                                def[i] = 0;
 
-                       var h = _luci2.NetworkModel._cache.bwstate[this.options.ifname] || { };
+                       var h = L.NetworkModel._cache.bwstate[this.options.ifname] || { };
                        return {
                                rx_bytes: (h.rx_bytes || def),
                                tx_bytes: (h.tx_bytes || def),
@@ -2273,35 +2492,35 @@ function LuCI2()
 
                removeFromInterface: function(iface)
                {
-                       if (!(iface instanceof _luci2.NetworkModel.Interface))
-                               iface = _luci2.NetworkModel.getInterface(iface);
+                       if (!(iface instanceof L.NetworkModel.Interface))
+                               iface = L.NetworkModel.getInterface(iface);
 
                        if (!iface)
                                return;
 
-                       var ifnames = _luci2.toArray(iface.get('ifname'));
+                       var ifnames = L.toArray(iface.get('ifname'));
                        if ($.inArray(this.options.ifname, ifnames) > -1)
-                               iface.set('ifname', _luci2.filterArray(ifnames, this.options.ifname));
+                               iface.set('ifname', L.filterArray(ifnames, this.options.ifname));
 
                        if (this.options.kind != 'wifi')
                                return;
 
-                       var networks = _luci2.toArray(this.get('network'));
+                       var networks = L.toArray(this.get('network'));
                        if ($.inArray(iface.name(), networks) > -1)
-                               this.set('network', _luci2.filterArray(networks, iface.name()));
+                               this.set('network', L.filterArray(networks, iface.name()));
                },
 
                attachToInterface: function(iface)
                {
-                       if (!(iface instanceof _luci2.NetworkModel.Interface))
-                               iface = _luci2.NetworkModel.getInterface(iface);
+                       if (!(iface instanceof L.NetworkModel.Interface))
+                               iface = L.NetworkModel.getInterface(iface);
 
                        if (!iface)
                                return;
 
                        if (this.options.kind != 'wifi')
                        {
-                               var ifnames = _luci2.toArray(iface.get('ifname'));
+                               var ifnames = L.toArray(iface.get('ifname'));
                                if ($.inArray(this.options.ifname, ifnames) < 0)
                                {
                                        ifnames.push(this.options.ifname);
@@ -2310,7 +2529,7 @@ function LuCI2()
                        }
                        else
                        {
-                               var networks = _luci2.toArray(this.get('network'));
+                               var networks = L.toArray(this.get('network'));
                                if ($.inArray(iface.name(), networks) < 0)
                                {
                                        networks.push(iface.name());
@@ -2323,7 +2542,7 @@ function LuCI2()
        this.NetworkModel.Interface = Class.extend({
                _status: function(key)
                {
-                       var s = _luci2.NetworkModel._cache.ifstate;
+                       var s = L.NetworkModel._cache.ifstate;
 
                        for (var i = 0; i < s.length; i++)
                                if (s[i]['interface'] == this.options.name)
@@ -2334,12 +2553,12 @@ function LuCI2()
 
                get: function(key)
                {
-                       return _luci2.NetworkModel._get('network', this.options.name, key);
+                       return L.NetworkModel._get('network', this.options.name, key);
                },
 
                set: function(key, val)
                {
-                       return _luci2.NetworkModel._set('network', this.options.name, key, val);
+                       return L.NetworkModel._set('network', this.options.name, key, val);
                },
 
                name: function()
@@ -2365,7 +2584,7 @@ function LuCI2()
                getProtocol: function()
                {
                        var prname = this.get('proto') || 'none';
-                       return _luci2.NetworkModel._protos[prname] || _luci2.NetworkModel._protos.none;
+                       return L.NetworkModel._protos[prname] || L.NetworkModel._protos.none;
                },
 
                getUptime: function()
@@ -2377,7 +2596,7 @@ function LuCI2()
                getDevice: function(resolveAlias)
                {
                        if (this.options.l3dev)
-                               return _luci2.NetworkModel.getDevice(this.options.l3dev);
+                               return L.NetworkModel.getDevice(this.options.l3dev);
 
                        return undefined;
                },
@@ -2385,7 +2604,7 @@ function LuCI2()
                getPhysdev: function()
                {
                        if (this.options.l2dev)
-                               return _luci2.NetworkModel.getDevice(this.options.l2dev);
+                               return L.NetworkModel.getDevice(this.options.l2dev);
 
                        return undefined;
                },
@@ -2394,11 +2613,11 @@ function LuCI2()
                {
                        var rv = [ ];
                        var dev = this.options.l2dev ?
-                               _luci2.NetworkModel._devs[this.options.l2dev] : undefined;
+                               L.NetworkModel._devs[this.options.l2dev] : undefined;
 
                        if (dev && dev.kind == 'bridge' && dev.ports && dev.ports.length)
                                for (var i = 0; i < dev.ports.length; i++)
-                                       rv.push(_luci2.NetworkModel.getDevice(dev.ports[i]));
+                                       rv.push(L.NetworkModel.getDevice(dev.ports[i]));
 
                        return rv;
                },
@@ -2508,16 +2727,40 @@ function LuCI2()
 
                getStatistics: function()
                {
-                       var dev = this.getDevice() || new _luci2.NetworkModel.Device({});
+                       var dev = this.getDevice() || new L.NetworkModel.Device({});
                        return dev.getStatistics();
                },
 
                getTrafficHistory: function()
                {
-                       var dev = this.getDevice() || new _luci2.NetworkModel.Device({});
+                       var dev = this.getDevice() || new L.NetworkModel.Device({});
                        return dev.getTrafficHistory();
                },
 
+               renderBadge: function()
+               {
+                       var badge = $('<span />')
+                               .addClass('badge')
+                               .text('%s: '.format(this.name()));
+
+                       var dev = this.getDevice();
+                       var subdevs = this.getSubdevices();
+
+                       if (subdevs.length)
+                               for (var j = 0; j < subdevs.length; j++)
+                                       badge.append($('<img />')
+                                               .attr('src', subdevs[j].icon())
+                                               .attr('title', '%s (%s)'.format(subdevs[j].description(), subdevs[j].name() || '?')));
+                       else if (dev)
+                               badge.append($('<img />')
+                                       .attr('src', dev.icon())
+                                       .attr('title', '%s (%s)'.format(dev.description(), dev.name() || '?')));
+                       else
+                               badge.append($('<em />').text(L.tr('(No devices attached)')));
+
+                       return badge;
+               },
+
                setDevices: function(devs)
                {
                        var dev = this.getPhysdev();
@@ -2536,7 +2779,7 @@ function LuCI2()
                                {
                                        var dev = devs[i];
 
-                                       if (dev instanceof _luci2.NetworkModel.Device)
+                                       if (dev instanceof L.NetworkModel.Device)
                                                dev = dev.name();
 
                                        if (!dev || old_devs[i].name() != dev)
@@ -2555,8 +2798,8 @@ function LuCI2()
                                {
                                        var dev = devs[i];
 
-                                       if (!(dev instanceof _luci2.NetworkModel.Device))
-                                               dev = _luci2.NetworkModel.getDevice(dev);
+                                       if (!(dev instanceof L.NetworkModel.Device))
+                                               dev = L.NetworkModel.getDevice(dev);
 
                                        if (dev)
                                                dev.attachToInterface(this);
@@ -2566,7 +2809,7 @@ function LuCI2()
 
                changeProtocol: function(proto)
                {
-                       var pr = _luci2.NetworkModel._protos[proto];
+                       var pr = L.NetworkModel._protos[proto];
 
                        if (!pr)
                                return;
@@ -2604,55 +2847,55 @@ function LuCI2()
                        var device = self.getDevice();
 
                        if (!mapwidget)
-                               mapwidget = _luci2.cbi.Map;
+                               mapwidget = L.cbi.Map;
 
                        var map = new mapwidget('network', {
-                               caption:     _luci2.tr('Configure "%s"').format(self.name())
+                               caption:     L.tr('Configure "%s"').format(self.name())
                        });
 
-                       var section = map.section(_luci2.cbi.SingleSection, self.name(), {
+                       var section = map.section(L.cbi.SingleSection, self.name(), {
                                anonymous:   true
                        });
 
                        section.tab({
                                id:      'general',
-                               caption: _luci2.tr('General Settings')
+                               caption: L.tr('General Settings')
                        });
 
                        section.tab({
                                id:      'advanced',
-                               caption: _luci2.tr('Advanced Settings')
+                               caption: L.tr('Advanced Settings')
                        });
 
                        section.tab({
                                id:      'ipv6',
-                               caption: _luci2.tr('IPv6')
+                               caption: L.tr('IPv6')
                        });
 
                        section.tab({
                                id:      'physical',
-                               caption: _luci2.tr('Physical Settings')
+                               caption: L.tr('Physical Settings')
                        });
 
 
-                       section.taboption('general', _luci2.cbi.CheckboxValue, 'auto', {
-                               caption:     _luci2.tr('Start on boot'),
+                       section.taboption('general', L.cbi.CheckboxValue, 'auto', {
+                               caption:     L.tr('Start on boot'),
                                optional:    true,
                                initial:     true
                        });
 
-                       var pr = section.taboption('general', _luci2.cbi.ListValue, 'proto', {
-                               caption:     _luci2.tr('Protocol')
+                       var pr = section.taboption('general', L.cbi.ListValue, 'proto', {
+                               caption:     L.tr('Protocol')
                        });
 
                        pr.ucivalue = function(sid) {
                                return self.get('proto') || 'none';
                        };
 
-                       var ok = section.taboption('general', _luci2.cbi.ButtonValue, '_confirm', {
-                               caption:     _luci2.tr('Really switch?'),
-                               description: _luci2.tr('Changing the protocol will clear all configuration for this interface!'),
-                               text:        _luci2.tr('Change protocol')
+                       var ok = section.taboption('general', L.cbi.ButtonValue, '_confirm', {
+                               caption:     L.tr('Really switch?'),
+                               description: L.tr('Changing the protocol will clear all configuration for this interface!'),
+                               text:        L.tr('Change protocol')
                        });
 
                        ok.on('click', function(ev) {
@@ -2660,7 +2903,7 @@ function LuCI2()
                                self.createForm(mapwidget).show();
                        });
 
-                       var protos = _luci2.NetworkModel.getProtocols();
+                       var protos = L.NetworkModel.getProtocols();
 
                        for (var i = 0; i < protos.length; i++)
                                pr.value(protos[i].name, protos[i].description);
@@ -2669,29 +2912,29 @@ function LuCI2()
 
                        if (!proto.virtual)
                        {
-                               var br = section.taboption('physical', _luci2.cbi.CheckboxValue, 'type', {
-                                       caption:     _luci2.tr('Network bridge'),
-                                       description: _luci2.tr('Merges multiple devices into one logical bridge'),
+                               var br = section.taboption('physical', L.cbi.CheckboxValue, 'type', {
+                                       caption:     L.tr('Network bridge'),
+                                       description: L.tr('Merges multiple devices into one logical bridge'),
                                        optional:    true,
                                        enabled:     'bridge',
                                        disabled:    '',
                                        initial:     ''
                                });
 
-                               section.taboption('physical', _luci2.cbi.DeviceList, '__iface_multi', {
-                                       caption:     _luci2.tr('Devices'),
+                               section.taboption('physical', L.cbi.DeviceList, '__iface_multi', {
+                                       caption:     L.tr('Devices'),
                                        multiple:    true,
                                        bridges:     false
                                }).depends('type', true);
 
-                               section.taboption('physical', _luci2.cbi.DeviceList, '__iface_single', {
-                                       caption:     _luci2.tr('Device'),
+                               section.taboption('physical', L.cbi.DeviceList, '__iface_single', {
+                                       caption:     L.tr('Device'),
                                        multiple:    false,
                                        bridges:     true
                                }).depends('type', false);
 
-                               var mac = section.taboption('physical', _luci2.cbi.InputValue, 'macaddr', {
-                                       caption:     _luci2.tr('Override MAC'),
+                               var mac = section.taboption('physical', L.cbi.InputValue, 'macaddr', {
+                                       caption:     L.tr('Override MAC'),
                                        optional:    true,
                                        placeholder: device ? device.getMACAddress() : undefined,
                                        datatype:    'macaddr'
@@ -2719,15 +2962,15 @@ function LuCI2()
                                };
                        }
 
-                       section.taboption('physical', _luci2.cbi.InputValue, 'mtu', {
-                               caption:     _luci2.tr('Override MTU'),
+                       section.taboption('physical', L.cbi.InputValue, 'mtu', {
+                               caption:     L.tr('Override MTU'),
                                optional:    true,
                                placeholder: device ? device.getMTU() : undefined,
                                datatype:    'range(1, 9000)'
                        });
 
-                       section.taboption('physical', _luci2.cbi.InputValue, 'metric', {
-                               caption:     _luci2.tr('Override Metric'),
+                       section.taboption('physical', L.cbi.InputValue, 'metric', {
+                               caption:     L.tr('Override Metric'),
                                optional:    true,
                                placeholder: 0,
                                datatype:    'uinteger'
@@ -2768,19 +3011,19 @@ function LuCI2()
        });
 
        this.system = {
-               getSystemInfo: _luci2.rpc.declare({
+               getSystemInfo: L.rpc.declare({
                        object: 'system',
                        method: 'info',
                        expect: { '': { } }
                }),
 
-               getBoardInfo: _luci2.rpc.declare({
+               getBoardInfo: L.rpc.declare({
                        object: 'system',
                        method: 'board',
                        expect: { '': { } }
                }),
 
-               getDiskInfo: _luci2.rpc.declare({
+               getDiskInfo: L.rpc.declare({
                        object: 'luci2.system',
                        method: 'diskfree',
                        expect: { '': { } }
@@ -2788,13 +3031,13 @@ function LuCI2()
 
                getInfo: function(cb)
                {
-                       _luci2.rpc.batch();
+                       L.rpc.batch();
 
                        this.getSystemInfo();
                        this.getBoardInfo();
                        this.getDiskInfo();
 
-                       return _luci2.rpc.flush().then(function(info) {
+                       return L.rpc.flush().then(function(info) {
                                var rv = { };
 
                                $.extend(rv, info[0]);
@@ -2805,43 +3048,8 @@ function LuCI2()
                        });
                },
 
-               getProcessList: _luci2.rpc.declare({
-                       object: 'luci2.system',
-                       method: 'process_list',
-                       expect: { processes: [ ] },
-                       filter: function(data) {
-                               data.sort(function(a, b) { return a.pid - b.pid });
-                               return data;
-                       }
-               }),
-
-               getSystemLog: _luci2.rpc.declare({
-                       object: 'luci2.system',
-                       method: 'syslog',
-                       expect: { log: '' }
-               }),
-
-               getKernelLog: _luci2.rpc.declare({
-                       object: 'luci2.system',
-                       method: 'dmesg',
-                       expect: { log: '' }
-               }),
-
-               getZoneInfo: function(cb)
-               {
-                       return $.getJSON(_luci2.globals.resource + '/zoneinfo.json', cb);
-               },
-
-               sendSignal: _luci2.rpc.declare({
-                       object: 'luci2.system',
-                       method: 'process_signal',
-                       params: [ 'pid', 'signal' ],
-                       filter: function(data) {
-                               return (data == 0);
-                       }
-               }),
 
-               initList: _luci2.rpc.declare({
+               initList: L.rpc.declare({
                        object: 'luci2.system',
                        method: 'init_list',
                        expect: { initscripts: [ ] },
@@ -2862,7 +3070,7 @@ function LuCI2()
                        });
                },
 
-               initRun: _luci2.rpc.declare({
+               initRun: L.rpc.declare({
                        object: 'luci2.system',
                        method: 'init_action',
                        params: [ 'name', 'action' ],
@@ -2871,257 +3079,30 @@ function LuCI2()
                        }
                }),
 
-               initStart:   function(init, cb) { return _luci2.system.initRun(init, 'start',   cb) },
-               initStop:    function(init, cb) { return _luci2.system.initRun(init, 'stop',    cb) },
-               initRestart: function(init, cb) { return _luci2.system.initRun(init, 'restart', cb) },
-               initReload:  function(init, cb) { return _luci2.system.initRun(init, 'reload',  cb) },
-               initEnable:  function(init, cb) { return _luci2.system.initRun(init, 'enable',  cb) },
-               initDisable: function(init, cb) { return _luci2.system.initRun(init, 'disable', cb) },
-
-
-               getRcLocal: _luci2.rpc.declare({
-                       object: 'luci2.system',
-                       method: 'rclocal_get',
-                       expect: { data: '' }
-               }),
-
-               setRcLocal: _luci2.rpc.declare({
-                       object: 'luci2.system',
-                       method: 'rclocal_set',
-                       params: [ 'data' ]
-               }),
-
-
-               getCrontab: _luci2.rpc.declare({
-                       object: 'luci2.system',
-                       method: 'crontab_get',
-                       expect: { data: '' }
-               }),
-
-               setCrontab: _luci2.rpc.declare({
-                       object: 'luci2.system',
-                       method: 'crontab_set',
-                       params: [ 'data' ]
-               }),
-
-
-               getSSHKeys: _luci2.rpc.declare({
-                       object: 'luci2.system',
-                       method: 'sshkeys_get',
-                       expect: { keys: [ ] }
-               }),
-
-               setSSHKeys: _luci2.rpc.declare({
-                       object: 'luci2.system',
-                       method: 'sshkeys_set',
-                       params: [ 'keys' ]
-               }),
+               initStart:   function(init, cb) { return L.system.initRun(init, 'start',   cb) },
+               initStop:    function(init, cb) { return L.system.initRun(init, 'stop',    cb) },
+               initRestart: function(init, cb) { return L.system.initRun(init, 'restart', cb) },
+               initReload:  function(init, cb) { return L.system.initRun(init, 'reload',  cb) },
+               initEnable:  function(init, cb) { return L.system.initRun(init, 'enable',  cb) },
+               initDisable: function(init, cb) { return L.system.initRun(init, 'disable', cb) },
 
 
-               setPassword: _luci2.rpc.declare({
-                       object: 'luci2.system',
-                       method: 'password_set',
-                       params: [ 'user', 'password' ]
-               }),
-
-
-               listLEDs: _luci2.rpc.declare({
-                       object: 'luci2.system',
-                       method: 'led_list',
-                       expect: { leds: [ ] }
-               }),
-
-               listUSBDevices: _luci2.rpc.declare({
-                       object: 'luci2.system',
-                       method: 'usb_list',
-                       expect: { devices: [ ] }
-               }),
-
-
-               testUpgrade: _luci2.rpc.declare({
-                       object: 'luci2.system',
-                       method: 'upgrade_test',
-                       expect: { '': { } }
-               }),
-
-               startUpgrade: _luci2.rpc.declare({
-                       object: 'luci2.system',
-                       method: 'upgrade_start',
-                       params: [ 'keep' ]
-               }),
-
-               cleanUpgrade: _luci2.rpc.declare({
-                       object: 'luci2.system',
-                       method: 'upgrade_clean'
-               }),
-
-
-               restoreBackup: _luci2.rpc.declare({
-                       object: 'luci2.system',
-                       method: 'backup_restore'
-               }),
-
-               cleanBackup: _luci2.rpc.declare({
-                       object: 'luci2.system',
-                       method: 'backup_clean'
-               }),
-
-
-               getBackupConfig: _luci2.rpc.declare({
-                       object: 'luci2.system',
-                       method: 'backup_config_get',
-                       expect: { config: '' }
-               }),
-
-               setBackupConfig: _luci2.rpc.declare({
-                       object: 'luci2.system',
-                       method: 'backup_config_set',
-                       params: [ 'data' ]
-               }),
-
-
-               listBackup: _luci2.rpc.declare({
-                       object: 'luci2.system',
-                       method: 'backup_list',
-                       expect: { files: [ ] }
-               }),
-
-
-               testReset: _luci2.rpc.declare({
-                       object: 'luci2.system',
-                       method: 'reset_test',
-                       expect: { supported: false }
-               }),
-
-               startReset: _luci2.rpc.declare({
-                       object: 'luci2.system',
-                       method: 'reset_start'
-               }),
-
-
-               performReboot: _luci2.rpc.declare({
+               performReboot: L.rpc.declare({
                        object: 'luci2.system',
                        method: 'reboot'
                })
        };
 
-       this.opkg = {
-               updateLists: _luci2.rpc.declare({
-                       object: 'luci2.opkg',
-                       method: 'update',
-                       expect: { '': { } }
-               }),
-
-               _allPackages: _luci2.rpc.declare({
-                       object: 'luci2.opkg',
-                       method: 'list',
-                       params: [ 'offset', 'limit', 'pattern' ],
-                       expect: { '': { } }
-               }),
-
-               _installedPackages: _luci2.rpc.declare({
-                       object: 'luci2.opkg',
-                       method: 'list_installed',
-                       params: [ 'offset', 'limit', 'pattern' ],
-                       expect: { '': { } }
-               }),
-
-               _findPackages: _luci2.rpc.declare({
-                       object: 'luci2.opkg',
-                       method: 'find',
-                       params: [ 'offset', 'limit', 'pattern' ],
-                       expect: { '': { } }
-               }),
-
-               _fetchPackages: function(action, offset, limit, pattern)
-               {
-                       var packages = [ ];
-
-                       return action(offset, limit, pattern).then(function(list) {
-                               if (!list.total || !list.packages)
-                                       return { length: 0, total: 0 };
-
-                               packages.push.apply(packages, list.packages);
-                               packages.total = list.total;
-
-                               if (limit <= 0)
-                                       limit = list.total;
-
-                               if (packages.length >= limit)
-                                       return packages;
-
-                               _luci2.rpc.batch();
-
-                               for (var i = offset + packages.length; i < limit; i += 100)
-                                       action(i, (Math.min(i + 100, limit) % 100) || 100, pattern);
-
-                               return _luci2.rpc.flush();
-                       }).then(function(lists) {
-                               for (var i = 0; i < lists.length; i++)
-                               {
-                                       if (!lists[i].total || !lists[i].packages)
-                                               continue;
-
-                                       packages.push.apply(packages, lists[i].packages);
-                                       packages.total = lists[i].total;
-                               }
-
-                               return packages;
-                       });
-               },
-
-               listPackages: function(offset, limit, pattern)
-               {
-                       return _luci2.opkg._fetchPackages(_luci2.opkg._allPackages, offset, limit, pattern);
-               },
-
-               installedPackages: function(offset, limit, pattern)
-               {
-                       return _luci2.opkg._fetchPackages(_luci2.opkg._installedPackages, offset, limit, pattern);
-               },
-
-               findPackages: function(offset, limit, pattern)
-               {
-                       return _luci2.opkg._fetchPackages(_luci2.opkg._findPackages, offset, limit, pattern);
-               },
-
-               installPackage: _luci2.rpc.declare({
-                       object: 'luci2.opkg',
-                       method: 'install',
-                       params: [ 'package' ],
-                       expect: { '': { } }
-               }),
-
-               removePackage: _luci2.rpc.declare({
-                       object: 'luci2.opkg',
-                       method: 'remove',
-                       params: [ 'package' ],
-                       expect: { '': { } }
-               }),
-
-               getConfig: _luci2.rpc.declare({
-                       object: 'luci2.opkg',
-                       method: 'config_get',
-                       expect: { config: '' }
-               }),
-
-               setConfig: _luci2.rpc.declare({
-                       object: 'luci2.opkg',
-                       method: 'config_set',
-                       params: [ 'data' ]
-               })
-       };
-
        this.session = {
 
-               login: _luci2.rpc.declare({
+               login: L.rpc.declare({
                        object: 'session',
                        method: 'login',
                        params: [ 'username', 'password' ],
                        expect: { '': { } }
                }),
 
-               access: _luci2.rpc.declare({
+               access: L.rpc.declare({
                        object: 'session',
                        method: 'access',
                        params: [ 'scope', 'object', 'function' ],
@@ -3130,21 +3111,21 @@ function LuCI2()
 
                isAlive: function()
                {
-                       return _luci2.session.access('ubus', 'session', 'access');
+                       return L.session.access('ubus', 'session', 'access');
                },
 
                startHeartbeat: function()
                {
                        this._hearbeatInterval = window.setInterval(function() {
-                               _luci2.session.isAlive().then(function(alive) {
+                               L.session.isAlive().then(function(alive) {
                                        if (!alive)
                                        {
-                                               _luci2.session.stopHeartbeat();
-                                               _luci2.ui.login(true);
+                                               L.session.stopHeartbeat();
+                                               L.ui.login(true);
                                        }
 
                                });
-                       }, _luci2.globals.timeout * 2);
+                       }, L.globals.timeout * 2);
                },
 
                stopHeartbeat: function()
@@ -3159,7 +3140,7 @@ function LuCI2()
 
                _acls: { },
 
-               _fetch_acls: _luci2.rpc.declare({
+               _fetch_acls: L.rpc.declare({
                        object: 'session',
                        method: 'access',
                        expect: { '': { } }
@@ -3167,18 +3148,18 @@ function LuCI2()
 
                _fetch_acls_cb: function(acls)
                {
-                       _luci2.session._acls = acls;
+                       L.session._acls = acls;
                },
 
                updateACLs: function()
                {
-                       return _luci2.session._fetch_acls()
-                               .then(_luci2.session._fetch_acls_cb);
+                       return L.session._fetch_acls()
+                               .then(L.session._fetch_acls_cb);
                },
 
                hasACL: function(scope, object, func)
                {
-                       var acls = _luci2.session._acls;
+                       var acls = L.session._acls;
 
                        if (typeof(func) == 'undefined')
                                return (acls && acls[scope] && acls[scope][object]);
@@ -3214,7 +3195,7 @@ function LuCI2()
                        var win = $(window);
                        var body = $('body');
 
-                       var state = _luci2.ui._loading || (_luci2.ui._loading = {
+                       var state = L.ui._loading || (L.ui._loading = {
                                modal: $('<div />')
                                        .css('z-index', 2000)
                                        .addClass('modal fade')
@@ -3224,7 +3205,7 @@ function LuCI2()
                                                        .addClass('modal-content luci2-modal-loader')
                                                        .append($('<div />')
                                                                .addClass('modal-body')
-                                                               .text(_luci2.tr('Loading data…')))))
+                                                               .text(L.tr('Loading data…')))))
                                        .appendTo(body)
                                        .modal({
                                                backdrop: 'static',
@@ -3240,7 +3221,7 @@ function LuCI2()
                        var win = $(window);
                        var body = $('body');
 
-                       var state = _luci2.ui._dialog || (_luci2.ui._dialog = {
+                       var state = L.ui._dialog || (L.ui._dialog = {
                                dialog: $('<div />')
                                        .addClass('modal fade')
                                        .append($('<div />')
@@ -3255,7 +3236,7 @@ function LuCI2()
                                                                .addClass('modal-body'))
                                                        .append($('<div />')
                                                                .addClass('modal-footer')
-                                                               .append(_luci2.ui.button(_luci2.tr('Close'), 'primary')
+                                                               .append(L.ui.button(L.tr('Close'), 'primary')
                                                                        .click(function() {
                                                                                $(this).parents('div.modal').modal('hide');
                                                                        })))))
@@ -3279,20 +3260,20 @@ function LuCI2()
 
                        if (options.style == 'confirm')
                        {
-                               ftr.append(_luci2.ui.button(_luci2.tr('Ok'), 'primary')
-                                       .click(options.confirm || function() { _luci2.ui.dialog(false) }));
+                               ftr.append(L.ui.button(L.tr('Ok'), 'primary')
+                                       .click(options.confirm || function() { L.ui.dialog(false) }));
 
-                               ftr.append(_luci2.ui.button(_luci2.tr('Cancel'), 'default')
-                                       .click(options.cancel || function() { _luci2.ui.dialog(false) }));
+                               ftr.append(L.ui.button(L.tr('Cancel'), 'default')
+                                       .click(options.cancel || function() { L.ui.dialog(false) }));
                        }
                        else if (options.style == 'close')
                        {
-                               ftr.append(_luci2.ui.button(_luci2.tr('Close'), 'primary')
-                                       .click(options.close || function() { _luci2.ui.dialog(false) }));
+                               ftr.append(L.ui.button(L.tr('Close'), 'primary')
+                                       .click(options.close || function() { L.ui.dialog(false) }));
                        }
                        else if (options.style == 'wait')
                        {
-                               ftr.append(_luci2.ui.button(_luci2.tr('Close'), 'primary')
+                               ftr.append(L.ui.button(L.tr('Close'), 'primary')
                                        .attr('disabled', true));
                        }
 
@@ -3315,7 +3296,7 @@ function LuCI2()
 
                upload: function(title, content, options)
                {
-                       var state = _luci2.ui._upload || (_luci2.ui._upload = {
+                       var state = L.ui._upload || (L.ui._upload = {
                                form: $('<form />')
                                        .attr('method', 'post')
                                        .attr('action', '/cgi-bin/luci-upload')
@@ -3357,17 +3338,17 @@ function LuCI2()
                                                json = $.parseJSON(body.innerHTML);
                                        } catch(e) {
                                                json = {
-                                                       message: _luci2.tr('Invalid server response received'),
-                                                       error: [ -1, _luci2.tr('Invalid data') ]
+                                                       message: L.tr('Invalid server response received'),
+                                                       error: [ -1, L.tr('Invalid data') ]
                                                };
                                        };
 
                                        if (json.error)
                                        {
                                                L.ui.dialog(L.tr('File upload'), [
-                                                       $('<p />').text(_luci2.tr('The file upload failed with the server response below:')),
+                                                       $('<p />').text(L.tr('The file upload failed with the server response below:')),
                                                        $('<pre />').addClass('alert-message').text(json.message || json.error[1]),
-                                                       $('<p />').text(_luci2.tr('In case of network problems try uploading the file again.'))
+                                                       $('<p />').text(L.tr('In case of network problems try uploading the file again.'))
                                                ], { style: 'close' });
                                        }
                                        else if (typeof(state.success_cb) == 'function')
@@ -3389,7 +3370,7 @@ function LuCI2()
 
                                        f.hide();
                                        b.show();
-                                       p.text(_luci2.tr('File upload in progress â€¦'));
+                                       p.text(L.tr('File upload in progress â€¦'));
 
                                        state.form.parent().parent().find('button').prop('disabled', true);
                                }
@@ -3397,14 +3378,14 @@ function LuCI2()
 
                        state.form.find('.progress').hide();
                        state.form.find('.cbi-input-file').val('').show();
-                       state.form.find('p').text(content || _luci2.tr('Select the file to upload and press "%s" to proceed.').format(_luci2.tr('Ok')));
+                       state.form.find('p').text(content || L.tr('Select the file to upload and press "%s" to proceed.').format(L.tr('Ok')));
 
-                       state.form.find('[name=sessionid]').val(_luci2.globals.sid);
+                       state.form.find('[name=sessionid]').val(L.globals.sid);
                        state.form.find('[name=filename]').val(options.filename);
 
                        state.success_cb = options.success;
 
-                       _luci2.ui.dialog(title || _luci2.tr('File upload'), state.form, {
+                       L.ui.dialog(title || L.tr('File upload'), state.form, {
                                style: 'confirm',
                                confirm: state.confirm_cb
                        });
@@ -3418,9 +3399,9 @@ function LuCI2()
                        var images    = $();
                        var interval, timeout;
 
-                       _luci2.ui.dialog(
-                               _luci2.tr('Waiting for device'), [
-                                       $('<p />').text(_luci2.tr('Please stand by while the device is reconfiguring â€¦')),
+                       L.ui.dialog(
+                               L.tr('Waiting for device'), [
+                                       $('<p />').text(L.tr('Please stand by while the device is reconfiguring â€¦')),
                                        $('<div />')
                                                .css('width', '100%')
                                                .addClass('progressbar')
@@ -3433,7 +3414,7 @@ function LuCI2()
                        for (var i = 0; i < protocols.length; i++)
                                images = images.add($('<img />').attr('url', protocols[i] + '://' + address + ':' + ports[i]));
 
-                       //_luci2.network.getNetworkStatus(function(s) {
+                       //L.network.getNetworkStatus(function(s) {
                        //      for (var i = 0; i < protocols.length; i++)
                        //      {
                        //              for (var j = 0; j < s.length; j++)
@@ -3448,12 +3429,12 @@ function LuCI2()
                        //}).then(function() {
                                images.on('load', function() {
                                        var url = this.getAttribute('url');
-                                       _luci2.session.isAlive().then(function(access) {
+                                       L.session.isAlive().then(function(access) {
                                                if (access)
                                                {
                                                        window.clearTimeout(timeout);
                                                        window.clearInterval(interval);
-                                                       _luci2.ui.dialog(false);
+                                                       L.ui.dialog(false);
                                                        images = null;
                                                }
                                                else
@@ -3465,7 +3446,7 @@ function LuCI2()
 
                                interval = window.setInterval(function() {
                                        images.each(function() {
-                                               this.setAttribute('src', this.getAttribute('url') + _luci2.globals.resource + '/icons/loading.gif?r=' + Math.random());
+                                               this.setAttribute('src', this.getAttribute('url') + L.globals.resource + '/icons/loading.gif?r=' + Math.random());
                                        });
                                }, 5000);
 
@@ -3473,9 +3454,9 @@ function LuCI2()
                                        window.clearInterval(interval);
                                        images.off('load');
 
-                                       _luci2.ui.dialog(
-                                               _luci2.tr('Device not responding'),
-                                               _luci2.tr('The device was not responding within 180 seconds, you might need to manually reconnect your computer or use SSH to regain access.'),
+                                       L.ui.dialog(
+                                               L.tr('Device not responding'),
+                                               L.tr('The device was not responding within 180 seconds, you might need to manually reconnect your computer or use SSH to regain access.'),
                                                { style: 'close' }
                                        );
                                }, 180000);
@@ -3484,16 +3465,16 @@ function LuCI2()
 
                login: function(invalid)
                {
-                       var state = _luci2.ui._login || (_luci2.ui._login = {
+                       var state = L.ui._login || (L.ui._login = {
                                form: $('<form />')
                                        .attr('target', '')
                                        .attr('method', 'post')
                                        .append($('<p />')
                                                .addClass('alert-message')
-                                               .text(_luci2.tr('Wrong username or password given!')))
+                                               .text(L.tr('Wrong username or password given!')))
                                        .append($('<p />')
                                                .append($('<label />')
-                                                       .text(_luci2.tr('Username'))
+                                                       .text(L.tr('Username'))
                                                        .append($('<br />'))
                                                        .append($('<input />')
                                                                .attr('type', 'text')
@@ -3506,7 +3487,7 @@ function LuCI2()
                                                                }))))
                                        .append($('<p />')
                                                .append($('<label />')
-                                                       .text(_luci2.tr('Password'))
+                                                       .text(L.tr('Password'))
                                                        .append($('<br />'))
                                                        .append($('<input />')
                                                                .attr('type', 'password')
@@ -3517,19 +3498,19 @@ function LuCI2()
                                                                                state.confirm_cb();
                                                                }))))
                                        .append($('<p />')
-                                               .text(_luci2.tr('Enter your username and password above, then click "%s" to proceed.').format(_luci2.tr('Ok')))),
+                                               .text(L.tr('Enter your username and password above, then click "%s" to proceed.').format(L.tr('Ok')))),
 
                                response_cb: function(response) {
                                        if (!response.ubus_rpc_session)
                                        {
-                                               _luci2.ui.login(true);
+                                               L.ui.login(true);
                                        }
                                        else
                                        {
-                                               _luci2.globals.sid = response.ubus_rpc_session;
-                                               _luci2.setHash('id', _luci2.globals.sid);
-                                               _luci2.session.startHeartbeat();
-                                               _luci2.ui.dialog(false);
+                                               L.globals.sid = response.ubus_rpc_session;
+                                               L.setHash('id', L.globals.sid);
+                                               L.session.startHeartbeat();
+                                               L.ui.dialog(false);
                                                state.deferred.resolve();
                                        }
                                },
@@ -3541,9 +3522,9 @@ function LuCI2()
                                        if (!u)
                                                return;
 
-                                       _luci2.ui.dialog(
-                                               _luci2.tr('Logging in'), [
-                                                       $('<p />').text(_luci2.tr('Log in in progress â€¦')),
+                                       L.ui.dialog(
+                                               L.tr('Logging in'), [
+                                                       $('<p />').text(L.tr('Log in in progress â€¦')),
                                                        $('<div />')
                                                                .css('width', '100%')
                                                                .addClass('progressbar')
@@ -3553,8 +3534,8 @@ function LuCI2()
                                                ], { style: 'wait' }
                                        );
 
-                                       _luci2.globals.sid = '00000000000000000000000000000000';
-                                       _luci2.session.login(u, p).then(state.response_cb);
+                                       L.globals.sid = '00000000000000000000000000000000';
+                                       L.session.login(u, p).then(state.response_cb);
                                }
                        });
 
@@ -3562,20 +3543,20 @@ function LuCI2()
                                state.deferred = $.Deferred();
 
                        /* try to find sid from hash */
-                       var sid = _luci2.getHash('id');
+                       var sid = L.getHash('id');
                        if (sid && sid.match(/^[a-f0-9]{32}$/))
                        {
-                               _luci2.globals.sid = sid;
-                               _luci2.session.isAlive().then(function(access) {
+                               L.globals.sid = sid;
+                               L.session.isAlive().then(function(access) {
                                        if (access)
                                        {
-                                               _luci2.session.startHeartbeat();
+                                               L.session.startHeartbeat();
                                                state.deferred.resolve();
                                        }
                                        else
                                        {
-                                               _luci2.setHash('id', undefined);
-                                               _luci2.ui.login();
+                                               L.setHash('id', undefined);
+                                               L.ui.login();
                                        }
                                });
 
@@ -3587,7 +3568,7 @@ function LuCI2()
                        else
                                state.form.find('.alert-message').hide();
 
-                       _luci2.ui.dialog(_luci2.tr('Authorization Required'), state.form, {
+                       L.ui.dialog(L.tr('Authorization Required'), state.form, {
                                style: 'confirm',
                                confirm: state.confirm_cb
                        });
@@ -3597,7 +3578,7 @@ function LuCI2()
                        return state.deferred;
                },
 
-               cryptPassword: _luci2.rpc.declare({
+               cryptPassword: L.rpc.declare({
                        object: 'luci2.ui',
                        method: 'crypt',
                        params: [ 'data' ],
@@ -3672,14 +3653,14 @@ function LuCI2()
                        }
                },
 
-               listAvailableACLs: _luci2.rpc.declare({
+               listAvailableACLs: L.rpc.declare({
                        object: 'luci2.ui',
                        method: 'acls',
                        expect: { acls: [ ] },
                        filter: function(trees) {
                                var acl_tree = { };
                                for (var i = 0; i < trees.length; i++)
-                                       _luci2.ui._acl_merge_tree(acl_tree, trees[i]);
+                                       L.ui._acl_merge_tree(acl_tree, trees[i]);
                                return acl_tree;
                        }
                }),
@@ -3696,18 +3677,18 @@ function LuCI2()
                                                        .addClass('label label-info'))));
                },
 
-               renderMainMenu: _luci2.rpc.declare({
+               renderMainMenu: L.rpc.declare({
                        object: 'luci2.ui',
                        method: 'menu',
                        expect: { menu: { } },
                        filter: function(entries) {
-                               _luci2.globals.mainMenu = new _luci2.ui.menu();
-                               _luci2.globals.mainMenu.entries(entries);
+                               L.globals.mainMenu = new L.ui.menu();
+                               L.globals.mainMenu.entries(entries);
 
                                $('#mainmenu')
                                        .empty()
-                                       .append(_luci2.globals.mainMenu.render(0, 1))
-                                       .append(_luci2.ui._render_change_indicator());
+                                       .append(L.globals.mainMenu.render(0, 1))
+                                       .append(L.ui._render_change_indicator());
                        }
                }),
 
@@ -3715,33 +3696,33 @@ function LuCI2()
                {
                        $('#viewmenu')
                                .empty()
-                               .append(_luci2.globals.mainMenu.render(2, 900));
+                               .append(L.globals.mainMenu.render(2, 900));
                },
 
                renderView: function()
                {
                        var node  = arguments[0];
                        var name  = node.view.split(/\//).join('.');
-                       var cname = _luci2.toClassName(name);
-                       var views = _luci2.views || (_luci2.views = { });
+                       var cname = L.toClassName(name);
+                       var views = L.views || (L.views = { });
                        var args  = [ ];
 
                        for (var i = 1; i < arguments.length; i++)
                                args.push(arguments[i]);
 
-                       if (_luci2.globals.currentView)
-                               _luci2.globals.currentView.finish();
+                       if (L.globals.currentView)
+                               L.globals.currentView.finish();
 
-                       _luci2.ui.renderViewMenu();
-                       _luci2.setHash('view', node.view);
+                       L.ui.renderViewMenu();
+                       L.setHash('view', node.view);
 
-                       if (views[cname] instanceof _luci2.ui.view)
+                       if (views[cname] instanceof L.ui.view)
                        {
-                               _luci2.globals.currentView = views[cname];
+                               L.globals.currentView = views[cname];
                                return views[cname].render.apply(views[cname], args);
                        }
 
-                       var url = _luci2.globals.resource + '/view/' + name + '.js';
+                       var url = L.globals.resource + '/view/' + name + '.js';
 
                        return $.ajax(url, {
                                method: 'GET',
@@ -3752,7 +3733,7 @@ function LuCI2()
                                        var viewConstructorSource = (
                                                '(function(L, $) { ' +
                                                        'return %s' +
-                                               '})(_luci2, $);\n\n' +
+                                               '})(L, $);\n\n' +
                                                '//@ sourceURL=%s'
                                        ).format(data, url);
 
@@ -3763,7 +3744,7 @@ function LuCI2()
                                                acls: node.write || { }
                                        });
 
-                                       _luci2.globals.currentView = views[cname];
+                                       L.globals.currentView = views[cname];
                                        return views[cname].render.apply(views[cname], args);
                                }
                                catch(e) {
@@ -3776,24 +3757,24 @@ function LuCI2()
 
                changeView: function()
                {
-                       var name = _luci2.getHash('view');
-                       var node = _luci2.globals.defaultNode;
+                       var name = L.getHash('view');
+                       var node = L.globals.defaultNode;
 
-                       if (name && _luci2.globals.mainMenu)
-                               node = _luci2.globals.mainMenu.getNode(name);
+                       if (name && L.globals.mainMenu)
+                               node = L.globals.mainMenu.getNode(name);
 
                        if (node)
                        {
-                               _luci2.ui.loading(true);
-                               _luci2.ui.renderView(node).then(function() {
-                                       _luci2.ui.loading(false);
+                               L.ui.loading(true);
+                               L.ui.renderView(node).then(function() {
+                                       L.ui.loading(false);
                                });
                        }
                },
 
                updateHostname: function()
                {
-                       return _luci2.system.getBoardInfo().then(function(info) {
+                       return L.system.getBoardInfo().then(function(info) {
                                if (info.hostname)
                                        $('#hostname').text(info.hostname);
                        });
@@ -3801,7 +3782,7 @@ function LuCI2()
 
                updateChanges: function()
                {
-                       return _luci2.uci.changes().then(function(changes) {
+                       return L.uci.changes().then(function(changes) {
                                var n = 0;
                                var html = '';
 
@@ -3861,10 +3842,10 @@ function LuCI2()
                                if (n > 0)
                                        $('#changes')
                                                .click(function(ev) {
-                                                       _luci2.ui.dialog(_luci2.tr('Staged configuration changes'), html, {
+                                                       L.ui.dialog(L.tr('Staged configuration changes'), html, {
                                                                style: 'confirm',
                                                                confirm: function() {
-                                                                       _luci2.uci.apply().then(
+                                                                       L.uci.apply().then(
                                                                                function(code) { alert('Success with code ' + code); },
                                                                                function(code) { alert('Error with code ' + code); }
                                                                        );
@@ -3874,7 +3855,7 @@ function LuCI2()
                                                })
                                                .children('span')
                                                        .show()
-                                                       .text(_luci2.trcp('Pending configuration changes', '1 change', '%d changes', n).format(n));
+                                                       .text(L.trcp('Pending configuration changes', '1 change', '%d changes', n).format(n));
                                else
                                        $('#changes').children('span').hide();
                        });
@@ -3882,21 +3863,21 @@ function LuCI2()
 
                init: function()
                {
-                       _luci2.ui.loading(true);
+                       L.ui.loading(true);
 
                        $.when(
-                               _luci2.session.updateACLs(),
-                               _luci2.ui.updateHostname(),
-                               _luci2.ui.updateChanges(),
-                               _luci2.ui.renderMainMenu(),
-                               _luci2.NetworkModel.init()
+                               L.session.updateACLs(),
+                               L.ui.updateHostname(),
+                               L.ui.updateChanges(),
+                               L.ui.renderMainMenu(),
+                               L.NetworkModel.init()
                        ).then(function() {
-                               _luci2.ui.renderView(_luci2.globals.defaultNode).then(function() {
-                                       _luci2.ui.loading(false);
+                               L.ui.renderView(L.globals.defaultNode).then(function() {
+                                       L.ui.loading(false);
                                });
 
                                $(window).on('hashchange', function() {
-                                       _luci2.ui.changeView();
+                                       L.ui.changeView();
                                });
                        });
                },
@@ -3948,13 +3929,40 @@ function LuCI2()
 
                appendTo: function(id) {
                        return $(id).append(this.render());
+               },
+
+               on: function(evname, evfunc)
+               {
+                       var evnames = L.toArray(evname);
+
+                       if (!this.events)
+                               this.events = { };
+
+                       for (var i = 0; i < evnames.length; i++)
+                               this.events[evnames[i]] = evfunc;
+
+                       return this;
+               },
+
+               trigger: function(evname, evdata)
+               {
+                       if (this.events)
+                       {
+                               var evnames = L.toArray(evname);
+
+                               for (var i = 0; i < evnames.length; i++)
+                                       if (this.events[evnames[i]])
+                                               this.events[evnames[i]].call(this, evdata);
+                       }
+
+                       return this;
                }
        });
 
        this.ui.view = this.ui.AbstractWidget.extend({
                _fetch_template: function()
                {
-                       return $.ajax(_luci2.globals.resource + '/template/' + this.options.name + '.htm', {
+                       return $.ajax(L.globals.resource + '/template/' + this.options.name + '.htm', {
                                method: 'GET',
                                cache: true,
                                dataType: 'text',
@@ -3967,10 +3975,10 @@ function LuCI2()
                                                        return '';
 
                                                case ':':
-                                                       return _luci2.tr(p2);
+                                                       return L.tr(p2);
 
                                                case '=':
-                                                       return _luci2.globals[p2] || '';
+                                                       return L.globals[p2] || '';
 
                                                default:
                                                        return '(?' + match + ')';
@@ -4006,7 +4014,7 @@ function LuCI2()
                                args.push(arguments[i]);
 
                        return this._fetch_template().then(function() {
-                               return _luci2.deferrable(self.execute.apply(self, args));
+                               return L.deferrable(self.execute.apply(self, args));
                        });
                },
 
@@ -4030,7 +4038,7 @@ function LuCI2()
                        };
 
                        runTimer = function() {
-                               _luci2.deferrable(func.call(self)).then(setTimer, setTimer);
+                               L.deferrable(func.call(self)).then(setTimer, setTimer);
                        };
 
                        runTimer();
@@ -4111,7 +4119,7 @@ function LuCI2()
 
                _onclick: function(ev)
                {
-                       _luci2.setHash('view', ev.data);
+                       L.setHash('view', ev.data);
 
                        ev.preventDefault();
                        this.blur();
@@ -4138,17 +4146,17 @@ function LuCI2()
 
                        for (var i = 0; i < nodes.length; i++)
                        {
-                               if (!_luci2.globals.defaultNode)
+                               if (!L.globals.defaultNode)
                                {
-                                       var v = _luci2.getHash('view');
+                                       var v = L.getHash('view');
                                        if (!v || v == nodes[i].view)
-                                               _luci2.globals.defaultNode = nodes[i];
+                                               L.globals.defaultNode = nodes[i];
                                }
 
                                var item = $('<li />')
                                        .append($('<a />')
                                                .attr('href', '#')
-                                               .text(_luci2.tr(nodes[i].title)))
+                                               .text(L.tr(nodes[i].title)))
                                        .appendTo(list);
 
                                if (nodes[i].childs && level < max)
@@ -4173,7 +4181,7 @@ function LuCI2()
 
                render: function(min, max)
                {
-                       var top = min ? this.getNode(_luci2.globals.defaultNode.view, min) : this._nodes;
+                       var top = min ? this.getNode(L.globals.defaultNode.view, min) : this._nodes;
                        return this._render(top.childs, 0, min, max);
                },
 
@@ -4421,46 +4429,46 @@ function LuCI2()
                                }
 
                                span.appendChild(document.createElement('img'));
-                               span.lastChild.src = _luci2.globals.resource + '/icons/signal-' + r + '.png';
+                               span.lastChild.src = L.globals.resource + '/icons/signal-' + r + '.png';
 
                                if (r == 'none')
-                                       span.title = _luci2.tr('No signal');
+                                       span.title = L.tr('No signal');
                                else
                                        span.title = '%s: %d %s / %s: %d %s'.format(
-                                               _luci2.tr('Signal'), this.options.signal, _luci2.tr('dBm'),
-                                               _luci2.tr('Noise'), this.options.noise, _luci2.tr('dBm')
+                                               L.tr('Signal'), this.options.signal, L.tr('dBm'),
+                                               L.tr('Noise'), this.options.noise, L.tr('dBm')
                                        );
                        }
                        else
                        {
                                var type = 'ethernet';
-                               var desc = _luci2.tr('Ethernet device');
+                               var desc = L.tr('Ethernet device');
 
                                if (l3dev != l2dev)
                                {
                                        type = 'tunnel';
-                                       desc = _luci2.tr('Tunnel interface');
+                                       desc = L.tr('Tunnel interface');
                                }
                                else if (dev.indexOf('br-') == 0)
                                {
                                        type = 'bridge';
-                                       desc = _luci2.tr('Bridge');
+                                       desc = L.tr('Bridge');
                                }
                                else if (dev.indexOf('.') > 0)
                                {
                                        type = 'vlan';
-                                       desc = _luci2.tr('VLAN interface');
+                                       desc = L.tr('VLAN interface');
                                }
                                else if (dev.indexOf('wlan') == 0 ||
                                                 dev.indexOf('ath') == 0 ||
                                                 dev.indexOf('wl') == 0)
                                {
                                        type = 'wifi';
-                                       desc = _luci2.tr('Wireless Network');
+                                       desc = L.tr('Wireless Network');
                                }
 
                                span.appendChild(document.createElement('img'));
-                               span.lastChild.src = _luci2.globals.resource + '/icons/' + type + (this.options.up ? '' : '_disabled') + '.png';
+                               span.lastChild.src = L.globals.resource + '/icons/' + type + (this.options.up ? '' : '_disabled') + '.png';
                                span.title = desc;
                        }
 
@@ -4481,7 +4489,7 @@ function LuCI2()
                validation: {
                        i18n: function(msg)
                        {
-                               _luci2.cbi.validation.message = _luci2.tr(msg);
+                               L.cbi.validation.message = L.tr(msg);
                        },
 
                        compile: function(code)
@@ -4489,7 +4497,7 @@ function LuCI2()
                                var pos = 0;
                                var esc = false;
                                var depth = 0;
-                               var types = _luci2.cbi.validation.types;
+                               var types = L.cbi.validation.types;
                                var stack = [ ];
 
                                code += ',';
@@ -4549,7 +4557,7 @@ function LuCI2()
                                                                throw "Syntax error, argument list follows non-function";
 
                                                        stack[stack.length-1] =
-                                                               _luci2.cbi.validation.compile(code.substring(pos, i));
+                                                               L.cbi.validation.compile(code.substring(pos, i));
 
                                                        pos = i+1;
                                                }
@@ -4603,8 +4611,7 @@ function LuCI2()
 
                'ipaddr': function()
                {
-                       if (validation.types['ip4addr'].apply(this) ||
-                               validation.types['ip6addr'].apply(this))
+                       if (L.parseIPv4(this) || L.parseIPv6(this))
                                return true;
 
                        validation.i18n('Must be a valid IP address');
@@ -4613,17 +4620,8 @@ function LuCI2()
 
                'ip4addr': function()
                {
-                       if (this.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(\/(\S+))?$/))
-                       {
-                               if ((RegExp.$1 >= 0) && (RegExp.$1 <= 255) &&
-                                   (RegExp.$2 >= 0) && (RegExp.$2 <= 255) &&
-                                   (RegExp.$3 >= 0) && (RegExp.$3 <= 255) &&
-                                   (RegExp.$4 >= 0) && (RegExp.$4 <= 255) &&
-                                   ((RegExp.$6.indexOf('.') < 0)
-                                     ? ((RegExp.$6 >= 0) && (RegExp.$6 <= 32))
-                                     : (validation.types['ip4addr'].apply(RegExp.$6))))
-                                       return true;
-                       }
+                       if (L.parseIPv4(this))
+                               return true;
 
                        validation.i18n('Must be a valid IPv4 address');
                        return false;
@@ -4631,62 +4629,74 @@ function LuCI2()
 
                'ip6addr': function()
                {
-                       if (this.match(/^([a-fA-F0-9:.]+)(\/(\d+))?$/))
-                       {
-                               if (!RegExp.$2 || ((RegExp.$3 >= 0) && (RegExp.$3 <= 128)))
-                               {
-                                       var addr = RegExp.$1;
+                       if (L.parseIPv6(this))
+                               return true;
 
-                                       if (addr == '::')
-                                       {
-                                               return true;
-                                       }
+                       validation.i18n('Must be a valid IPv6 address');
+                       return false;
+               },
 
-                                       if (addr.indexOf('.') > 0)
-                                       {
-                                               var off = addr.lastIndexOf(':');
+               'netmask4': function()
+               {
+                       if (L.isNetmask(L.parseIPv4(this)))
+                               return true;
 
-                                               if (!(off && validation.types['ip4addr'].apply(addr.substr(off+1))))
-                                               {
-                                                       validation.i18n('Must be a valid IPv6 address');
-                                                       return false;
-                                               }
+                       validation.i18n('Must be a valid IPv4 netmask');
+                       return false;
+               },
+
+               'netmask6': function()
+               {
+                       if (L.isNetmask(L.parseIPv6(this)))
+                               return true;
 
-                                               addr = addr.substr(0, off) + ':0:0';
-                                       }
+                       validation.i18n('Must be a valid IPv6 netmask6');
+                       return false;
+               },
 
-                                       if (addr.indexOf('::') >= 0)
-                                       {
-                                               var colons = 0;
-                                               var fill = '0';
+               'cidr4': function()
+               {
+                       if (this.match(/^([0-9.]+)\/(\d{1,2})$/))
+                               if (RegExp.$2 <= 32 && L.parseIPv4(RegExp.$1))
+                                       return true;
 
-                                               for (var i = 1; i < (addr.length-1); i++)
-                                                       if (addr.charAt(i) == ':')
-                                                               colons++;
+                       validation.i18n('Must be a valid IPv4 prefix');
+                       return false;
+               },
 
-                                               if (colons > 7)
-                                               {
-                                                       validation.i18n('Must be a valid IPv6 address');
-                                                       return false;
-                                               }
+               'cidr6': function()
+               {
+                       if (this.match(/^([a-fA-F0-9:.]+)\/(\d{1,3})$/))
+                               if (RegExp.$2 <= 128 && L.parseIPv6(RegExp.$1))
+                                       return true;
 
-                                               for (var i = 0; i < (7 - colons); i++)
-                                                       fill += ':0';
+                       validation.i18n('Must be a valid IPv6 prefix');
+                       return false;
+               },
 
-                                               if (addr.match(/^(.*?)::(.*?)$/))
-                                                       addr = (RegExp.$1 ? RegExp.$1 + ':' : '') + fill +
-                                                                  (RegExp.$2 ? ':' + RegExp.$2 : '');
-                                       }
+               'ipmask4': function()
+               {
+                       if (this.match(/^([0-9.]+)\/([0-9.]+)$/))
+                       {
+                               var addr = RegExp.$1, mask = RegExp.$2;
+                               if (L.parseIPv4(addr) && L.isNetmask(L.parseIPv4(mask)))
+                                       return true;
+                       }
 
-                                       if (addr.match(/^(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}$/) != null)
-                                               return true;
+                       validation.i18n('Must be a valid IPv4 address/netmask pair');
+                       return false;
+               },
 
-                                       validation.i18n('Must be a valid IPv6 address');
-                                       return false;
-                               }
+               'ipmask6': function()
+               {
+                       if (this.match(/^([a-fA-F0-9:.]+)\/([a-fA-F0-9:.]+)$/))
+                       {
+                               var addr = RegExp.$1, mask = RegExp.$2;
+                               if (L.parseIPv6(addr) && L.isNetmask(L.parseIPv6(mask)))
+                                       return true;
                        }
 
-                       validation.i18n('Must be a valid IPv6 address');
+                       validation.i18n('Must be a valid IPv6 address/netmask pair');
                        return false;
                },
 
@@ -4893,7 +4903,7 @@ function LuCI2()
                                        msgs.push(validation.message.format.apply(validation.message, arguments[i+1]));
                        }
 
-                       validation.message = msgs.join( _luci2.tr(' - or - '));
+                       validation.message = msgs.join( L.tr(' - or - '));
                        return false;
                },
 
@@ -4967,7 +4977,7 @@ function LuCI2()
                        this.dependencies = [ ];
                        this.rdependency = { };
 
-                       this.options = _luci2.defaults(options, {
+                       this.options = L.defaults(options, {
                                placeholder: '',
                                datatype: 'string',
                                optional: false,
@@ -5021,6 +5031,11 @@ function LuCI2()
                        return i.top;
                },
 
+               active: function(sid)
+               {
+                       return (this.instance[sid] && !this.instance[sid].disabled);
+               },
+
                ucipath: function(sid)
                {
                        return {
@@ -5067,9 +5082,9 @@ function LuCI2()
                                                return this.choices[i][1];
                        }
                        else if (v === true)
-                               return _luci2.tr('yes');
+                               return L.tr('yes');
                        else if (v === false)
-                               return _luci2.tr('no');
+                               return L.tr('no');
                        else if ($.isArray(v))
                                return v.join(', ');
 
@@ -5084,7 +5099,7 @@ function LuCI2()
                        if (typeof(a) != typeof(b))
                                return true;
 
-                       if (typeof(a) == 'object')
+                       if ($.isArray(a))
                        {
                                if (a.length != b.length)
                                        return true;
@@ -5095,6 +5110,18 @@ function LuCI2()
 
                                return false;
                        }
+                       else if ($.isPlainObject(a))
+                       {
+                               for (var k in a)
+                                       if (!(k in b))
+                                               return true;
+
+                               for (var k in b)
+                                       if (!(k in a) || a[k] !== b[k])
+                                               return true;
+
+                               return false;
+                       }
 
                        return (a != b);
                },
@@ -5136,7 +5163,7 @@ function LuCI2()
                                        d.elem.parents('div.form-group, td').first().addClass('luci2-form-error');
                                        d.elem.parents('div.input-group, div.form-group, td').first().addClass('has-error');
 
-                                       d.inst.error.text(_luci2.tr('Field must not be empty')).show();
+                                       d.inst.error.text(L.tr('Field must not be empty')).show();
                                        rv = false;
                                }
                                else if (val.length > 0 && !vstack[0].apply(val, vstack[1]))
@@ -5160,14 +5187,31 @@ function LuCI2()
                        }
 
                        if (rv)
+                       {
                                for (var field in d.self.rdependency)
                                        d.self.rdependency[field].toggle(d.sid);
 
+                               d.self.section.tabtoggle(d.sid);
+                       }
+
                        return rv;
                },
 
                validator: function(sid, elem, multi)
                {
+                       var evdata = {
+                               self:   this,
+                               sid:    sid,
+                               elem:   elem,
+                               multi:  multi,
+                               inst:   this.instance[sid],
+                               opt:    this.options.optional
+                       };
+
+                       if (this.events)
+                               for (var evname in this.events)
+                                       elem.on(evname, evdata, this.events[evname]);
+
                        if (typeof(this.options.datatype) == 'undefined' && $.isEmptyObject(this.rdependency))
                                return elem;
 
@@ -5175,13 +5219,13 @@ function LuCI2()
                        if (typeof(this.options.datatype) == 'string')
                        {
                                try {
-                                       vstack = _luci2.cbi.validation.compile(this.options.datatype);
+                                       evdata.vstack = L.cbi.validation.compile(this.options.datatype);
                                } catch(e) { };
                        }
                        else if (typeof(this.options.datatype) == 'function')
                        {
                                var vfunc = this.options.datatype;
-                               vstack = [ function(elem) {
+                               evdata.vstack = [ function(elem) {
                                        var rv = vfunc(this, elem);
                                        if (rv !== true)
                                                validation.message = rv;
@@ -5189,16 +5233,6 @@ function LuCI2()
                                }, [ elem ] ];
                        }
 
-                       var evdata = {
-                               self:   this,
-                               sid:    sid,
-                               elem:   elem,
-                               multi:  multi,
-                               vstack: vstack,
-                               inst:   this.instance[sid],
-                               opt:    this.options.optional
-                       };
-
                        if (elem.prop('tagName') == 'SELECT')
                        {
                                elem.change(evdata, this._ev_validate);
@@ -5228,7 +5262,7 @@ function LuCI2()
                        return (i.disabled || i.error.text() == '');
                },
 
-               depends: function(d, v)
+               depends: function(d, v, add)
                {
                        var dep;
 
@@ -5239,11 +5273,11 @@ function LuCI2()
                                {
                                        if (typeof(d[i]) == 'string')
                                                dep[d[i]] = true;
-                                       else if (d[i] instanceof _luci2.cbi.AbstractValue)
+                                       else if (d[i] instanceof L.cbi.AbstractValue)
                                                dep[d[i].name] = true;
                                }
                        }
-                       else if (d instanceof _luci2.cbi.AbstractValue)
+                       else if (d instanceof L.cbi.AbstractValue)
                        {
                                dep = { };
                                dep[d.name] = (typeof(v) == 'undefined') ? true : v;
@@ -5273,7 +5307,11 @@ function LuCI2()
                        if ($.isEmptyObject(dep))
                                return this;
 
-                       this.dependencies.push(dep);
+                       if (!add || !this.dependencies.length)
+                               this.dependencies.push(dep);
+                       else
+                               for (var i = 0; i < this.dependencies.length; i++)
+                                       $.extend(this.dependencies[i], dep);
 
                        return this;
                },
@@ -5303,7 +5341,7 @@ function LuCI2()
                                                        break;
                                                }
                                        }
-                                       else if (typeof(cmp) == 'string')
+                                       else if (typeof(cmp) == 'string' || typeof(cmp) == 'number')
                                        {
                                                if (val != cmp)
                                                {
@@ -5442,12 +5480,12 @@ function LuCI2()
 
                        var t = $('<span />')
                                .addClass('input-group-btn')
-                               .append(_luci2.ui.button(_luci2.tr('Reveal'), 'default')
+                               .append(L.ui.button(L.tr('Reveal'), 'default')
                                        .click(function(ev) {
                                                var b = $(this);
                                                var i = b.parent().prev();
                                                var t = i.attr('type');
-                                               b.text(t == 'password' ? _luci2.tr('Hide') : _luci2.tr('Reveal'));
+                                               b.text(t == 'password' ? L.tr('Hide') : L.tr('Reveal'));
                                                i.attr('type', (t == 'password') ? 'text' : 'password');
                                                b = i = t = null;
                                        }));
@@ -5467,10 +5505,10 @@ function LuCI2()
                        var s = $('<select />')
                                .addClass('form-control');
 
-                       if (this.options.optional)
+                       if (this.options.optional && !this.has_empty)
                                $('<option />')
                                        .attr('value', '')
-                                       .text(_luci2.tr('-- Please choose --'))
+                                       .text(L.tr('-- Please choose --'))
                                        .appendTo(s);
 
                        if (this.choices)
@@ -5490,6 +5528,9 @@ function LuCI2()
                        if (!this.choices)
                                this.choices = [ ];
 
+                       if (k == '')
+                               this.has_empty = true;
+
                        this.choices.push([k, v || k]);
                        return this;
                }
@@ -5564,10 +5605,7 @@ function LuCI2()
                        {
                                ev.data.select.hide();
                                ev.data.input.show().focus();
-
-                               var v = ev.data.input.val();
-                               ev.data.input.val(' ');
-                               ev.data.input.val(v);
+                               ev.data.input.val('');
                        }
                        else if (self.options.optional && s.selectedIndex == 0)
                        {
@@ -5577,6 +5615,8 @@ function LuCI2()
                        {
                                ev.data.input.val(ev.data.select.val());
                        }
+
+                       ev.stopPropagation();
                },
 
                _blur: function(ev)
@@ -5587,10 +5627,10 @@ function LuCI2()
 
                        ev.data.select.empty();
 
-                       if (self.options.optional)
+                       if (self.options.optional && !self.has_empty)
                                $('<option />')
                                        .attr('value', '')
-                                       .text(_luci2.tr('-- please choose --'))
+                                       .text(L.tr('-- please choose --'))
                                        .appendTo(ev.data.select);
 
                        if (self.choices)
@@ -5613,11 +5653,11 @@ function LuCI2()
 
                        $('<option />')
                                .attr('value', ' ')
-                               .text(_luci2.tr('-- custom --'))
+                               .text(L.tr('-- custom --'))
                                .appendTo(ev.data.select);
 
                        ev.data.input.hide();
-                       ev.data.select.val(val).show().focus();
+                       ev.data.select.val(val).show().blur();
                },
 
                _enter: function(ev)
@@ -5636,17 +5676,19 @@ function LuCI2()
                                .attr('id', this.id(sid));
 
                        var t = $('<input />')
+                               .addClass('form-control')
                                .attr('type', 'text')
                                .hide()
                                .appendTo(d);
 
                        var s = $('<select />')
+                               .addClass('form-control')
                                .appendTo(d);
 
                        var evdata = {
                                self: this,
-                               input: this.validator(sid, t),
-                               select: this.validator(sid, s)
+                               input: t,
+                               select: s
                        };
 
                        s.change(evdata, this._change);
@@ -5656,6 +5698,9 @@ function LuCI2()
                        t.val(this.ucivalue(sid));
                        t.blur();
 
+                       this.validator(sid, t);
+                       this.validator(sid, s);
+
                        return d;
                },
 
@@ -5664,6 +5709,9 @@ function LuCI2()
                        if (!this.choices)
                                this.choices = [ ];
 
+                       if (k == '')
+                               this.has_empty = true;
+
                        this.choices.push([k, v || k]);
                        return this;
                },
@@ -5711,9 +5759,9 @@ function LuCI2()
 
                                var btn;
                                if (evdata.remove)
-                                       btn = _luci2.ui.button('–', 'danger').click(evdata, this._btnclick);
+                                       btn = L.ui.button('–', 'danger').click(evdata, this._btnclick);
                                else
-                                       btn = _luci2.ui.button('+', 'success').click(evdata, this._btnclick);
+                                       btn = L.ui.button('+', 'success').click(evdata, this._btnclick);
 
                                if (this.choices)
                                {
@@ -5925,7 +5973,7 @@ function LuCI2()
                formvalue: function(sid)
                {
                        var rv = [ ];
-                       var fields = $('#' + this.id(sid) + ' input');
+                       var fields = $('#' + this.id(sid) + ' input');
 
                        for (var i = 0; i < fields.length; i++)
                                if (typeof(fields[i].value) == 'string' && fields[i].value.length)
@@ -5941,7 +5989,7 @@ function LuCI2()
                        return $('<div />')
                                .addClass('form-control-static')
                                .attr('id', this.id(sid))
-                               .html(this.ucivalue(sid));
+                               .html(this.ucivalue(sid) || this.label('placeholder'));
                },
 
                formvalue: function(sid)
@@ -5968,7 +6016,7 @@ function LuCI2()
        this.cbi.NetworkList = this.cbi.AbstractValue.extend({
                load: function(sid)
                {
-                       return _luci2.NetworkModel.init();
+                       return L.NetworkModel.init();
                },
 
                _device_icon: function(dev)
@@ -5995,35 +6043,21 @@ function LuCI2()
                                for (var i = 0; i < value.length; i++)
                                        check[value[i]] = true;
 
-                       var interfaces = _luci2.NetworkModel.getInterfaces();
+                       var interfaces = L.NetworkModel.getInterfaces();
 
                        for (var i = 0; i < interfaces.length; i++)
                        {
                                var iface = interfaces[i];
-                               var badge = $('<span />')
-                                       .addClass('badge')
-                                       .text('%s: '.format(iface.name()));
-
-                               var dev = iface.getDevice();
-                               var subdevs = iface.getSubdevices();
-
-                               if (subdevs.length)
-                                       for (var j = 0; j < subdevs.length; j++)
-                                               badge.append(this._device_icon(subdevs[j]));
-                               else if (dev)
-                                       badge.append(this._device_icon(dev));
-                               else
-                                       badge.append($('<em />').text(_luci2.tr('(No devices attached)')));
 
                                $('<li />')
                                        .append($('<label />')
                                                .addClass(itype + ' inline')
-                                               .append($('<input />')
+                                               .append(this.validator(sid, $('<input />')
                                                        .attr('name', itype + id)
                                                        .attr('type', itype)
                                                        .attr('value', iface.name())
-                                                       .prop('checked', !!check[iface.name()]))
-                                               .append(badge))
+                                                       .prop('checked', !!check[iface.name()]), true))
+                                               .append(iface.renderBadge()))
                                        .appendTo(ul);
                        }
 
@@ -6032,12 +6066,12 @@ function LuCI2()
                                $('<li />')
                                        .append($('<label />')
                                                .addClass(itype + ' inline text-muted')
-                                               .append($('<input />')
+                                               .append(this.validator(sid, $('<input />')
                                                        .attr('name', itype + id)
                                                        .attr('type', itype)
                                                        .attr('value', '')
-                                                       .prop('checked', $.isEmptyObject(check)))
-                                               .append(_luci2.tr('unspecified')))
+                                                       .prop('checked', $.isEmptyObject(check)), true))
+                                               .append(L.tr('unspecified')))
                                        .appendTo(ul);
                        }
 
@@ -6094,6 +6128,168 @@ function LuCI2()
                }
        });
 
+       this.cbi.DeviceList = this.cbi.NetworkList.extend({
+               _ev_focus: function(ev)
+               {
+                       var self = ev.data.self;
+                       var input = $(this);
+
+                       input.parent().prev().prop('checked', true);
+               },
+
+               _ev_blur: function(ev)
+               {
+                       ev.which = 10;
+                       ev.data.self._ev_keydown.call(this, ev);
+               },
+
+               _ev_keydown: function(ev)
+               {
+                       if (ev.which != 10 && ev.which != 13)
+                               return;
+
+                       var sid = ev.data.sid;
+                       var self = ev.data.self;
+                       var input = $(this);
+                       var ifnames = L.toArray(input.val());
+
+                       if (!ifnames.length)
+                               return;
+
+                       L.NetworkModel.createDevice(ifnames[0]);
+
+                       self._redraw(sid, $('#' + self.id(sid)), ifnames[0]);
+               },
+
+               load: function(sid)
+               {
+                       return L.NetworkModel.init();
+               },
+
+               _redraw: function(sid, ul, sel)
+               {
+                       var id = ul.attr('id');
+                       var devs = L.NetworkModel.getDevices();
+                       var iface = L.NetworkModel.getInterface(sid);
+                       var itype = this.options.multiple ? 'checkbox' : 'radio';
+                       var check = { };
+
+                       if (!sel)
+                       {
+                               for (var i = 0; i < devs.length; i++)
+                                       if (devs[i].isInNetwork(iface))
+                                               check[devs[i].name()] = true;
+                       }
+                       else
+                       {
+                               if (this.options.multiple)
+                                       check = L.toObject(this.formvalue(sid));
+
+                               check[sel] = true;
+                       }
+
+                       ul.empty();
+
+                       for (var i = 0; i < devs.length; i++)
+                       {
+                               var dev = devs[i];
+
+                               if (dev.isBridge() && this.options.bridges === false)
+                                       continue;
+
+                               if (!dev.isBridgeable() && this.options.multiple)
+                                       continue;
+
+                               var badge = $('<span />')
+                                       .addClass('badge')
+                                       .append($('<img />').attr('src', dev.icon()))
+                                       .append(' %s: %s'.format(dev.name(), dev.description()));
+
+                               //var ifcs = dev.getInterfaces();
+                               //if (ifcs.length)
+                               //{
+                               //      for (var j = 0; j < ifcs.length; j++)
+                               //              badge.append((j ? ', ' : ' (') + ifcs[j].name());
+                               //
+                               //      badge.append(')');
+                               //}
+
+                               $('<li />')
+                                       .append($('<label />')
+                                               .addClass(itype + ' inline')
+                                               .append($('<input />')
+                                                       .attr('name', itype + id)
+                                                       .attr('type', itype)
+                                                       .attr('value', dev.name())
+                                                       .prop('checked', !!check[dev.name()]))
+                                               .append(badge))
+                                       .appendTo(ul);
+                       }
+
+
+                       $('<li />')
+                               .append($('<label />')
+                                       .attr('for', 'custom' + id)
+                                       .addClass(itype + ' inline')
+                                       .append($('<input />')
+                                               .attr('name', itype + id)
+                                               .attr('type', itype)
+                                               .attr('value', ''))
+                                       .append($('<span />')
+                                               .addClass('badge')
+                                               .append($('<input />')
+                                                       .attr('id', 'custom' + id)
+                                                       .attr('type', 'text')
+                                                       .attr('placeholder', L.tr('Custom device â€¦'))
+                                                       .on('focus', { self: this, sid: sid }, this._ev_focus)
+                                                       .on('blur', { self: this, sid: sid }, this._ev_blur)
+                                                       .on('keydown', { self: this, sid: sid }, this._ev_keydown))))
+                               .appendTo(ul);
+
+                       if (!this.options.multiple)
+                       {
+                               $('<li />')
+                                       .append($('<label />')
+                                               .addClass(itype + ' inline text-muted')
+                                               .append($('<input />')
+                                                       .attr('name', itype + id)
+                                                       .attr('type', itype)
+                                                       .attr('value', '')
+                                                       .prop('checked', $.isEmptyObject(check)))
+                                               .append(L.tr('unspecified')))
+                                       .appendTo(ul);
+                       }
+               },
+
+               widget: function(sid)
+               {
+                       var id = this.id(sid);
+                       var ul = $('<ul />')
+                               .attr('id', id)
+                               .addClass('list-unstyled');
+
+                       this._redraw(sid, ul);
+
+                       return ul;
+               },
+
+               save: function(sid)
+               {
+                       if (this.instance[sid].disabled)
+                               return;
+
+                       var ifnames = this.formvalue(sid);
+                       //if (!ifnames)
+                       //      return;
+
+                       var iface = L.NetworkModel.getInterface(sid);
+                       if (!iface)
+                               return;
+
+                       iface.setDevices($.isArray(ifnames) ? ifnames : [ ifnames ]);
+               }
+       });
+
 
        this.cbi.AbstractSection = this.ui.AbstractWidget.extend({
                id: function()
@@ -6145,7 +6341,7 @@ function LuCI2()
 
                        var w = widget ? new widget(name, options) : null;
 
-                       if (!(w instanceof _luci2.cbi.AbstractValue))
+                       if (!(w instanceof L.cbi.AbstractValue))
                                throw 'Widget must be an instance of AbstractValue';
 
                        w.section = this;
@@ -6157,6 +6353,30 @@ function LuCI2()
                        return w;
                },
 
+               tabtoggle: function(sid)
+               {
+                       for (var i = 0; i < this.tabs.length; i++)
+                       {
+                               var tab = this.tabs[i];
+                               var elem = $('#' + this.id('nodetab', sid, tab.id));
+                               var empty = true;
+
+                               for (var j = 0; j < tab.fields.length; j++)
+                               {
+                                       if (tab.fields[j].active(sid))
+                                       {
+                                               empty = false;
+                                               break;
+                                       }
+                               }
+
+                               if (empty && elem.is(':visible'))
+                                       elem.fadeOut();
+                               else if (!empty)
+                                       elem.fadeIn();
+                       }
+               },
+
                ucipackages: function(pkg)
                {
                        for (var i = 0; i < this.tabs.length; i++)
@@ -6203,7 +6423,7 @@ function LuCI2()
                                if (inval > 0)
                                        stbadge.show()
                                                .text(inval)
-                                               .attr('title', _luci2.trp('1 Error', '%d Errors', inval).format(inval));
+                                               .attr('title', L.trp('1 Error', '%d Errors', inval).format(inval));
                                else
                                        stbadge.hide();
 
@@ -6213,7 +6433,7 @@ function LuCI2()
                        if (invals > 0)
                                badge.show()
                                        .text(invals)
-                                       .attr('title', _luci2.trp('1 Error', '%d Errors', invals).format(invals));
+                                       .attr('title', L.trp('1 Error', '%d Errors', invals).format(invals));
                        else
                                badge.hide();
 
@@ -6222,8 +6442,7 @@ function LuCI2()
 
                validate: function()
                {
-                       this.error_count = 0;
-
+                       var errors = 0;
                        var as = this.sections();
 
                        for (var i = 0; i < as.length; i++)
@@ -6231,19 +6450,19 @@ function LuCI2()
                                var invals = this.validate_section(as[i]['.name']);
 
                                if (invals > 0)
-                                       this.error_count += invals;
+                                       errors += invals;
                        }
 
                        var badge = $('#' + this.id('sectiontab')).children('span:first');
 
-                       if (this.error_count > 0)
+                       if (errors > 0)
                                badge.show()
-                                       .text(this.error_count)
-                                       .attr('title', _luci2.trp('1 Error', '%d Errors', this.error_count).format(this.error_count));
+                                       .text(errors)
+                                       .attr('title', L.trp('1 Error', '%d Errors', errors).format(errors));
                        else
                                badge.hide();
 
-                       return (this.error_count == 0);
+                       return (errors == 0);
                }
        });
 
@@ -6254,7 +6473,6 @@ function LuCI2()
                        this.options  = options;
                        this.tabs     = [ ];
                        this.fields   = { };
-                       this.error_count  = 0;
                        this.active_panel = 0;
                        this.active_tab   = { };
                },
@@ -6264,9 +6482,14 @@ function LuCI2()
                        return true;
                },
 
+               sort: function(section1, section2)
+               {
+                       return 0;
+               },
+
                sections: function(cb)
                {
-                       var s1 = _luci2.uci.sections(this.map.uci_package);
+                       var s1 = L.uci.sections(this.map.uci_package);
                        var s2 = [ ];
 
                        for (var i = 0; i < s1.length; i++)
@@ -6274,21 +6497,23 @@ function LuCI2()
                                        if (this.filter(s1[i]))
                                                s2.push(s1[i]);
 
+                       s2.sort(this.sort);
+
                        if (typeof(cb) == 'function')
                                for (var i = 0; i < s2.length; i++)
-                                       cb.apply(this, [ s2[i] ]);
+                                       cb.call(this, s2[i]);
 
                        return s2;
                },
 
                add: function(name)
                {
-                       this.map.add(this.map.uci_package, this.uci_type, name);
+                       return this.map.add(this.map.uci_package, this.uci_type, name);
                },
 
                remove: function(sid)
                {
-                       this.map.remove(this.map.uci_package, sid);
+                       return this.map.remove(this.map.uci_package, sid);
                },
 
                _ev_add: function(ev)
@@ -6303,14 +6528,20 @@ function LuCI2()
                        if (addb.prop('disabled') || name === '')
                                return;
 
-                       _luci2.ui.saveScrollTop();
+                       L.ui.saveScrollTop();
 
                        self.active_panel = -1;
                        self.map.save();
-                       self.add(name);
+
+                       ev.data.sid  = self.add(name);
+                       ev.data.type = self.uci_type;
+                       ev.data.name = name;
+
+                       self.trigger('add', ev);
+
                        self.map.redraw();
 
-                       _luci2.ui.restoreScrollTop();
+                       L.ui.restoreScrollTop();
                },
 
                _ev_remove: function(ev)
@@ -6318,13 +6549,15 @@ function LuCI2()
                        var self = ev.data.self;
                        var sid  = ev.data.sid;
 
-                       _luci2.ui.saveScrollTop();
+                       L.ui.saveScrollTop();
+
+                       self.trigger('remove', ev);
 
                        self.map.save();
                        self.remove(sid);
                        self.map.redraw();
 
-                       _luci2.ui.restoreScrollTop();
+                       L.ui.restoreScrollTop();
 
                        ev.stopPropagation();
                },
@@ -6339,15 +6572,15 @@ function LuCI2()
 
                        if (!/^[a-zA-Z0-9_]*$/.test(name))
                        {
-                               errt.text(_luci2.tr('Invalid section name')).show();
+                               errt.text(L.tr('Invalid section name')).show();
                                text.addClass('error');
                                addb.prop('disabled', true);
                                return false;
                        }
 
-                       if (_luci2.uci.get(self.map.uci_package, name))
+                       if (L.uci.get(self.map.uci_package, name))
                        {
-                               errt.text(_luci2.tr('Name already used')).show();
+                               errt.text(L.tr('Name already used')).show();
                                text.addClass('error');
                                addb.prop('disabled', true);
                                return false;
@@ -6414,7 +6647,7 @@ function LuCI2()
 
                        if (new_idx >= 0 && new_idx < s.length)
                        {
-                               _luci2.uci.swap(self.map.uci_package, s[cur_idx]['.name'], s[new_idx]['.name']);
+                               L.uci.swap(self.map.uci_package, s[cur_idx]['.name'], s[new_idx]['.name']);
 
                                self.map.save();
                                self.map.redraw();
@@ -6436,9 +6669,9 @@ function LuCI2()
                                        for (var i = 0; i < this.options.teasers.length; i++)
                                        {
                                                var f = this.options.teasers[i];
-                                               if (f instanceof _luci2.cbi.AbstractValue)
+                                               if (f instanceof L.cbi.AbstractValue)
                                                        tf.push(f);
-                                               else if (typeof(f) == 'string' && this.fields[f] instanceof _luci2.cbi.AbstractValue)
+                                               else if (typeof(f) == 'string' && this.fields[f] instanceof L.cbi.AbstractValue)
                                                        tf.push(this.fields[f]);
                                        }
                                }
@@ -6474,8 +6707,8 @@ function LuCI2()
                        if (!this.options.addremove)
                                return null;
 
-                       var text = _luci2.tr('Add section');
-                       var ttip = _luci2.tr('Create new section...');
+                       var text = L.tr('Add section');
+                       var ttip = L.tr('Create new section...');
 
                        if ($.isArray(this.options.add_caption))
                                text = this.options.add_caption[0], ttip = this.options.add_caption[1];
@@ -6495,7 +6728,7 @@ function LuCI2()
                                        .appendTo(add);
 
                                $('<img />')
-                                       .attr('src', _luci2.globals.resource + '/icons/cbi/add.gif')
+                                       .attr('src', L.globals.resource + '/icons/cbi/add.gif')
                                        .attr('title', text)
                                        .addClass('cbi-button')
                                        .click({ self: this }, this._ev_add)
@@ -6508,7 +6741,7 @@ function LuCI2()
                        }
                        else
                        {
-                               _luci2.ui.button(text, 'success', ttip)
+                               L.ui.button(text, 'success', ttip)
                                        .click({ self: this }, this._ev_add)
                                        .appendTo(add);
                        }
@@ -6521,15 +6754,15 @@ function LuCI2()
                        if (!this.options.addremove)
                                return null;
 
-                       var text = _luci2.tr('Remove');
-                       var ttip = _luci2.tr('Remove this section');
+                       var text = L.tr('Remove');
+                       var ttip = L.tr('Remove this section');
 
                        if ($.isArray(this.options.remove_caption))
                                text = this.options.remove_caption[0], ttip = this.options.remove_caption[1];
                        else if (typeof(this.options.remove_caption) == 'string')
                                text = this.options.remove_caption, ttip = '';
 
-                       return _luci2.ui.button(text, 'danger', ttip)
+                       return L.ui.button(text, 'danger', ttip)
                                .click({ self: this, sid: sid, index: index }, this._ev_remove);
                },
 
@@ -6538,10 +6771,10 @@ function LuCI2()
                        if (!this.options.sortable)
                                return null;
 
-                       var b1 = _luci2.ui.button('↑', 'info', _luci2.tr('Move up'))
+                       var b1 = L.ui.button('↑', 'info', L.tr('Move up'))
                                .click({ self: this, index: index, up: true }, this._ev_sort);
 
-                       var b2 = _luci2.ui.button('↓', 'info', _luci2.tr('Move down'))
+                       var b2 = L.ui.button('↓', 'info', L.tr('Move down'))
                                .click({ self: this, index: index, up: false }, this._ev_sort);
 
                        return b1.add(b2);
@@ -6625,6 +6858,9 @@ function LuCI2()
                        if (cur == tab_index)
                                tabh.addClass('active');
 
+                       if (!tab.fields.length)
+                               tabh.hide();
+
                        return tabh;
                },
 
@@ -6727,7 +6963,7 @@ function LuCI2()
                        {
                                body.append($('<li />')
                                        .addClass('list-group-item text-muted')
-                                       .text(this.label('placeholder') || _luci2.tr('There are no entries defined yet.')))
+                                       .text(this.label('placeholder') || L.tr('There are no entries defined yet.')))
                        }
 
                        for (var i = 0; i < s.length; i++)
@@ -6817,6 +7053,7 @@ function LuCI2()
                        if (this.options.addremove !== false || this.options.sortable)
                        {
                                row.append($('<td />')
+                                       .css('width', '1%')
                                        .addClass('text-right')
                                        .append($('<div />')
                                                .addClass('btn-group')
@@ -6844,7 +7081,7 @@ function LuCI2()
                                        .append($('<td />')
                                                .addClass('text-muted')
                                                .attr('colspan', cols)
-                                               .text(this.label('placeholder') || _luci2.tr('There are no entries defined yet.'))));
+                                               .text(this.label('placeholder') || L.tr('There are no entries defined yet.'))));
                        }
 
                        for (var i = 0; i < s.length; i++)
@@ -6871,7 +7108,7 @@ function LuCI2()
                sections: function(cb)
                {
                        var sa = [ ];
-                       var sl = _luci2.uci.sections(this.map.uci_package);
+                       var sl = L.uci.sections(this.map.uci_package);
 
                        for (var i = 0; i < sl.length; i++)
                                if (sl[i]['.name'] == this.uci_type)
@@ -6914,7 +7151,7 @@ function LuCI2()
 
                        this.uci_package = uci_package;
                        this.sections = [ ];
-                       this.options = _luci2.defaults(options, {
+                       this.options = L.defaults(options, {
                                save:    function() { },
                                prepare: function() { }
                        });
@@ -6922,7 +7159,7 @@ function LuCI2()
 
                _load_cb: function()
                {
-                       var deferreds = [ _luci2.deferrable(this.options.prepare()) ];
+                       var deferreds = [ L.deferrable(this.options.prepare()) ];
 
                        for (var i = 0; i < this.sections.length; i++)
                        {
@@ -6935,7 +7172,7 @@ function LuCI2()
                                        for (var j = 0; j < s.length; j++)
                                        {
                                                var rv = this.sections[i].fields[f].load(s[j]['.name']);
-                                               if (_luci2.isDeferred(rv))
+                                               if (L.isDeferred(rv))
                                                        deferreds.push(rv);
                                        }
                                }
@@ -6955,10 +7192,10 @@ function LuCI2()
                        packages[this.uci_package] = true;
 
                        for (var pkg in packages)
-                               if (!_luci2.uci.writable(pkg))
+                               if (!L.uci.writable(pkg))
                                        this.options.readonly = true;
 
-                       return _luci2.uci.load(_luci2.toArray(packages)).then(function() {
+                       return L.uci.load(L.toArray(packages)).then(function() {
                                return self._load_cb();
                        });
                },
@@ -6971,6 +7208,30 @@ function LuCI2()
                        self.active_tab = parseInt(ev.target.getAttribute('data-luci2-tab-index'));
                },
 
+               _ev_apply: function(ev)
+               {
+                       var self = ev.data.self;
+
+                       self.trigger('apply', ev);
+               },
+
+               _ev_save: function(ev)
+               {
+                       var self = ev.data.self;
+
+                       self.send().then(function() {
+                               self.trigger('save', ev);
+                       });
+               },
+
+               _ev_reset: function(ev)
+               {
+                       var self = ev.data.self;
+
+                       self.trigger('reset', ev);
+                       self.reset();
+               },
+
                _render_tab_head: function(tab_index)
                {
                        var section = this.sections[tab_index];
@@ -7045,16 +7306,20 @@ function LuCI2()
 
                _render_footer: function()
                {
+                       var evdata = {
+                               self: this
+                       };
+
                        return $('<div />')
                                .addClass('panel panel-default panel-body text-right')
                                .append($('<div />')
                                        .addClass('btn-group')
-                                       .append(_luci2.ui.button(_luci2.tr('Save & Apply'), 'primary')
-                                               .click({ self: this }, function(ev) {  }))
-                                       .append(_luci2.ui.button(_luci2.tr('Save'), 'default')
-                                               .click({ self: this }, function(ev) { ev.data.self.send(); }))
-                                       .append(_luci2.ui.button(_luci2.tr('Reset'), 'default')
-                                               .click({ self: this }, function(ev) { ev.data.self.insertInto(ev.data.self.target); })));
+                                       .append(L.ui.button(L.tr('Save & Apply'), 'primary')
+                                               .click(evdata, this._ev_apply))
+                                       .append(L.ui.button(L.tr('Save'), 'default')
+                                               .click(evdata, this._ev_save))
+                                       .append(L.ui.button(L.tr('Reset'), 'default')
+                                               .click(evdata, this._ev_reset)));
                },
 
                render: function()
@@ -7096,7 +7361,7 @@ function LuCI2()
                {
                        var w = widget ? new widget(uci_type, options) : null;
 
-                       if (!(w instanceof _luci2.cbi.AbstractSection))
+                       if (!(w instanceof L.cbi.AbstractSection))
                                throw 'Widget must be an instance of AbstractSection';
 
                        w.map = this;
@@ -7125,22 +7390,22 @@ function LuCI2()
 
                add: function(conf, type, name)
                {
-                       return _luci2.uci.add(conf, type, name);
+                       return L.uci.add(conf, type, name);
                },
 
                remove: function(conf, sid)
                {
-                       return _luci2.uci.remove(conf, sid);
+                       return L.uci.remove(conf, sid);
                },
 
                get: function(conf, sid, opt)
                {
-                       return _luci2.uci.get(conf, sid, opt);
+                       return L.uci.get(conf, sid, opt);
                },
 
                set: function(conf, sid, opt, val)
                {
-                       return _luci2.uci.set(conf, sid, opt, val);
+                       return L.uci.set(conf, sid, opt, val);
                },
 
                validate: function()
@@ -7161,7 +7426,7 @@ function LuCI2()
                        var self = this;
 
                        if (self.options.readonly)
-                               return _luci2.deferrable();
+                               return L.deferrable();
 
                        var deferreds = [ ];
 
@@ -7179,48 +7444,69 @@ function LuCI2()
                                        for (var j = 0; j < s.length; j++)
                                        {
                                                var rv = self.sections[i].fields[f].save(s[j]['.name']);
-                                               if (_luci2.isDeferred(rv))
+                                               if (L.isDeferred(rv))
                                                        deferreds.push(rv);
                                        }
                                }
                        }
 
                        return $.when.apply($, deferreds).then(function() {
-                               return _luci2.deferrable(self.options.save());
+                               return L.deferrable(self.options.save());
                        });
                },
 
                send: function()
                {
                        if (!this.validate())
-                               return _luci2.deferrable();
+                               return L.deferrable();
 
                        var self = this;
 
-                       _luci2.ui.saveScrollTop();
-                       _luci2.ui.loading(true);
+                       L.ui.saveScrollTop();
+                       L.ui.loading(true);
 
                        return this.save().then(function() {
-                               return _luci2.uci.save();
+                               return L.uci.save();
                        }).then(function() {
-                               return _luci2.ui.updateChanges();
+                               return L.ui.updateChanges();
                        }).then(function() {
                                return self.load();
                        }).then(function() {
                                self.redraw();
                                self = null;
 
-                               _luci2.ui.loading(false);
-                               _luci2.ui.restoreScrollTop();
+                               L.ui.loading(false);
+                               L.ui.restoreScrollTop();
                        });
                },
 
+               revert: function()
+               {
+                       var packages = { };
+
+                       for (var i = 0; i < this.sections.length; i++)
+                               this.sections[i].ucipackages(packages);
+
+                       packages[this.uci_package] = true;
+
+                       L.uci.unload(L.toArray(packages));
+               },
+
+               reset: function()
+               {
+                       var self = this;
+
+                       self.revert();
+
+                       return self.insertInto(self.target);
+               },
+
                insertInto: function(id)
                {
                        var self = this;
                            self.target = $(id);
 
-                       _luci2.ui.loading(true);
+                       L.ui.loading(true);
                        self.target.hide();
 
                        return self.load().then(function() {
@@ -7228,27 +7514,57 @@ function LuCI2()
                                self.finish();
                                self.target.show();
                                self = null;
-                               _luci2.ui.loading(false);
+                               L.ui.loading(false);
                        });
                }
        });
 
        this.cbi.Modal = this.cbi.Map.extend({
+               _ev_apply: function(ev)
+               {
+                       var self = ev.data.self;
+
+                       self.trigger('apply', ev);
+               },
+
+               _ev_save: function(ev)
+               {
+                       var self = ev.data.self;
+
+                       self.send().then(function() {
+                               self.trigger('save', ev);
+                               self.close();
+                       });
+               },
+
+               _ev_reset: function(ev)
+               {
+                       var self = ev.data.self;
+
+                       self.trigger('close', ev);
+                       self.revert();
+                       self.close();
+               },
+
                _render_footer: function()
                {
+                       var evdata = {
+                               self: this
+                       };
+
                        return $('<div />')
                                .addClass('btn-group')
-                               .append(_luci2.ui.button(_luci2.tr('Save & Apply'), 'primary')
-                                       .click({ self: this }, function(ev) {  }))
-                               .append(_luci2.ui.button(_luci2.tr('Save'), 'default')
-                                       .click({ self: this }, function(ev) { ev.data.self.send(); }))
-                               .append(_luci2.ui.button(_luci2.tr('Cancel'), 'default')
-                                       .click({ self: this }, function(ev) { _luci2.ui.dialog(false); }));
+                               .append(L.ui.button(L.tr('Save & Apply'), 'primary')
+                                       .click(evdata, this._ev_apply))
+                               .append(L.ui.button(L.tr('Save'), 'default')
+                                       .click(evdata, this._ev_save))
+                               .append(L.ui.button(L.tr('Cancel'), 'default')
+                                       .click(evdata, this._ev_reset));
                },
 
                render: function()
                {
-                       var modal = _luci2.ui.dialog(this.label('caption'), null, { wide: true });
+                       var modal = L.ui.dialog(this.label('caption'), null, { wide: true });
                        var map = $('<form />');
 
                        var desc = this.label('description');
@@ -7273,14 +7589,19 @@ function LuCI2()
                {
                        var self = this;
 
-                       _luci2.ui.loading(true);
+                       L.ui.loading(true);
 
                        return self.load().then(function() {
                                self.render();
                                self.finish();
 
-                               _luci2.ui.loading(false);
+                               L.ui.loading(false);
                        });
+               },
+
+               close: function()
+               {
+                       L.ui.dialog(false);
                }
        });
 };