luci2: implement LuCI2.session.updateACLs() and LuCI2.session.hasACL()
[project/luci2/ui.git] / luci2 / htdocs / luci2 / luci2.js
index a012e5f..58d38df 100644 (file)
@@ -177,52 +177,6 @@ function LuCI2()
 {
        var _luci2 = this;
 
-       var alphacmp = function(a, b)
-       {
-               if (a < b)
-                       return -1;
-               else if (a > b)
-                       return 1;
-               else
-                       return 0;
-       };
-
-       var retcb = function(cb, rv)
-       {
-               if (typeof(cb) == 'function')
-                       cb(rv);
-
-               return rv;
-       };
-
-       var isa = function(x, t)
-       {
-               if (typeof(x) != 'string' && typeof(t) == 'string')
-                       return (Object.prototype.toString.call(x) == '[object ' + t + ']');
-
-               return (Object.prototype.toString.call(x) == Object.prototype.toString.call(t));
-       };
-
-       var rcall = function(obj, func, params, res_attr, res_default, cb, filter)
-       {
-               if (typeof(params) == 'undefined')
-                       params = { };
-
-               return _luci2.rpc.call(obj, func, params).then(function(res) {
-                       if (res[0] != 0 || typeof(res[1]) == 'undefined')
-                               return retcb(cb, res_default);
-
-                       var rv = (typeof(res_attr) != 'undefined') ? res[1][res_attr] : res[1];
-                       if (typeof(rv) == 'undefined' || (typeof(res_default) != 'undefined' && !isa(rv, res_default)))
-                               return retcb(cb, res_default);
-
-                       if (typeof(filter) == 'function')
-                               rv = filter(rv);
-
-                       return retcb(cb, rv);
-               });
-       };
-
        var Class = function() { };
 
        Class.extend = function(properties)
@@ -260,7 +214,7 @@ function LuCI2()
                _class.prototype = prototype;
                _class.prototype.constructor = _class;
 
-               _class.extend = arguments.callee;
+               _class.extend = Class.extend;
 
                return _class;
        };
@@ -274,7 +228,7 @@ function LuCI2()
                return obj;
        };
 
-       this.deferred = function(x)
+       this.isDeferred = function(x)
        {
                return (typeof(x) == 'object' &&
                        typeof(x.then) == 'function' &&
@@ -283,7 +237,7 @@ function LuCI2()
 
        this.deferrable = function()
        {
-               if (this.deferred(arguments[0]))
+               if (this.isDeferred(arguments[0]))
                        return arguments[0];
 
                var d = $.Deferred();
@@ -410,8 +364,10 @@ function LuCI2()
                        h += keys[i] + ':' + data[keys[i]];
                }
 
-               if (h)
+               if (h.length)
                        location.hash = '#' + h;
+               else
+                       location.hash = '';
        };
 
        this.getHash = function(key)
@@ -432,600 +388,1337 @@ function LuCI2()
                return data;
        };
 
-       this.globals = {
-               resource: '/luci2'
+       this.toArray = function(x)
+       {
+               switch (typeof(x))
+               {
+               case 'number':
+               case 'boolean':
+                       return [ x ];
+
+               case 'string':
+                       var r = [ ];
+                       var l = x.split(/\s+/);
+                       for (var i = 0; i < l.length; i++)
+                               if (l[i].length > 0)
+                                       r.push(l[i]);
+                       return r;
+
+               case 'object':
+                       if ($.isArray(x))
+                       {
+                               var r = [ ];
+                               for (var i = 0; i < x.length; i++)
+                                       r.push(x[i]);
+                               return r;
+                       }
+                       else if ($.isPlainObject(x))
+                       {
+                               var r = [ ];
+                               for (var k in x)
+                                       if (x.hasOwnProperty(k))
+                                               r.push(k);
+                               return r.sort();
+                       }
+               }
+
+               return [ ];
        };
 
-       this.rpc = {
+       this.toObject = function(x)
+       {
+               switch (typeof(x))
+               {
+               case 'number':
+               case 'boolean':
+                       return { x: true };
+
+               case 'string':
+                       var r = { };
+                       var l = x.split(/\x+/);
+                       for (var i = 0; i < l.length; i++)
+                               if (l[i].length > 0)
+                                       r[l[i]] = true;
+                       return r;
+
+               case 'object':
+                       if ($.isArray(x))
+                       {
+                               var r = { };
+                               for (var i = 0; i < x.length; i++)
+                                       r[x[i]] = true;
+                               return r;
+                       }
+                       else if ($.isPlainObject(x))
+                       {
+                               return x;
+                       }
+               }
 
-               _msg_id: 1,
+               return { };
+       };
 
-               _wrap_msg: function(method, object, func, args)
-               {
-                       if (typeof(args) != 'object')
-                               args = { };
+       this.filterArray = function(array, item)
+       {
+               if (!$.isArray(array))
+                       return [ ];
 
-                       return {
-                               id:      _luci2.rpc._msg_id++,
-                               jsonrpc: "2.0",
-                               method:  method,
-                               params:  (method == 'call') ? [ _luci2.globals.sid, object, func, args ] : object
-                       };
-               },
+               for (var i = 0; i < array.length; i++)
+                       if (array[i] === item)
+                               array.splice(i--, 1);
 
-               _parse_response: function(keys, priv)
-               {
-                       return function(data) {
-                               var obj;
-                               try {
-                                       obj = $.parseJSON(data);
-                               } catch(e) { }
+               return array;
+       };
 
-                               if (typeof(obj) != 'object')
-                                       return undefined;
+       this.toClassName = function(str, suffix)
+       {
+               var n = '';
+               var l = str.split(/[\/.]/);
 
-                               /* is a batched response */
-                               if (keys)
-                               {
-                                       var rv = { };
-                                       for (var i = 0; i < obj.length; i++)
-                                       {
-                                               var p = (typeof(priv) != 'undefined') ? priv[i] : undefined;
+               for (var i = 0; i < l.length; i++)
+                       if (l[i].length > 0)
+                               n += l[i].charAt(0).toUpperCase() + l[i].substr(1).toLowerCase();
 
-                                               if ($.isArray(obj[i].result) && typeof(priv) != 'undefined')
-                                                       obj[i].result[2] = p;
+               if (typeof(suffix) == 'string')
+                       n += suffix;
 
-                                               if (obj[i].jsonrpc != '2.0' || obj[i].error || !obj[i].result)
-                                                       rv[keys[i]] = [ 4 /* UBUS_STATUS_NO_DATA */, undefined, p ];
-                                               else
-                                                       rv[keys[i]] = obj[i].result;
-                                       }
-                                       return rv;
-                               }
+               return n;
+       };
 
-                               if (obj.jsonrpc != '2.0' || obj.error || !obj.result)
-                                       return [ 4 /* UBUS_STATUS_NO_DATA */, undefined, priv ];
+       this.globals = {
+               timeout:  15000,
+               resource: '/luci2',
+               sid:      '00000000000000000000000000000000'
+       };
 
-                               if ($.isArray(obj.result) && typeof(priv) != 'undefined')
-                                       obj.result[2] = priv;
+       this.rpc = {
 
-                               return obj.result;
-                       };
-               },
+               _id: 1,
+               _batch: undefined,
+               _requests: { },
 
-               _post_msg: function(message, cb, keys, priv)
+               _call: function(req, cb)
                {
                        return $.ajax('/ubus', {
                                cache:       false,
                                contentType: 'application/json',
-                               data:        JSON.stringify(message),
-                               dataFilter:  _luci2.rpc._parse_response(keys, priv),
-                               dataType:    'text',
-                               success:     cb,
-                               type:        'POST'
-                       });
+                               data:        JSON.stringify(req),
+                               dataType:    'json',
+                               type:        'POST',
+                               timeout:     _luci2.globals.timeout,
+                               _rpc_req:   req
+                       }).then(cb, cb);
                },
 
-               _post_single: function(object, method, args, cb, priv)
+               _list_cb: function(msg)
                {
-                       var msg = _luci2.rpc._wrap_msg('call', object, method, args, priv);
-                       return _luci2.rpc._post_msg(msg, cb, undefined, priv);
+                       var list = msg.result;
+
+                       /* verify message frame */
+                       if (typeof(msg) != 'object' || msg.jsonrpc != '2.0' || !msg.id || !$.isArray(list))
+                               list = [ ];
+
+                       return $.Deferred().resolveWith(this, [ list ]);
                },
 
-               _post_batch: function(methods, cb)
+               _call_cb: function(msg)
                {
-                       if (typeof(methods) != 'object')
-                               return undefined;
+                       var data = [ ];
+                       var type = Object.prototype.toString;
+                       var reqs = this._rpc_req;
 
-                       var msgs = [ ];
-                       var keys = [ ];
-                       var priv = [ ];
+                       if (!$.isArray(reqs))
+                       {
+                               msg = [ msg ];
+                               reqs = [ reqs ];
+                       }
 
-                       for (var k in methods)
+                       for (var i = 0; i < msg.length; i++)
                        {
-                               if (typeof(methods[k]) != 'object' || methods[k].length < 2)
-                                       continue;
+                               /* fetch related request info */
+                               var req = _luci2.rpc._requests[reqs[i].id];
+                               if (typeof(req) != 'object')
+                                       throw 'No related request for JSON response';
 
-                               keys.push(k);
-                               priv.push(methods[k][3]);
-                               msgs.push(_luci2.rpc._wrap_msg('call', methods[k][0], methods[k][1], methods[k][2]));
-                       }
+                               /* fetch response attribute and verify returned type */
+                               var ret = undefined;
 
-                       if (msgs.length > 0)
-                               return _luci2.rpc._post_msg(msgs, cb, keys, priv);
+                               /* verify message frame */
+                               if (typeof(msg[i]) == 'object' && msg[i].jsonrpc == '2.0')
+                                       if ($.isArray(msg[i].result) && msg[i].result[0] == 0)
+                                               ret = (msg[i].result.length > 1) ? msg[i].result[1] : msg[i].result[0];
 
-                       return _luci2.deferrable([ ]);
-               },
+                               if (req.expect)
+                               {
+                                       for (var key in req.expect)
+                                       {
+                                               if (typeof(ret) != 'undefined' && key != '')
+                                                       ret = ret[key];
 
-               call: function()
-               {
-                       var a = arguments;
-                       if (typeof a[0] == 'string')
-                               return _luci2.rpc._post_single(a[0], a[1], a[2], a[3], a[4]);
-                       else
-                               return _luci2.rpc._post_batch(a[0], a[1]);
-               },
+                                               if (typeof(ret) == 'undefined' || type.call(ret) != type.call(req.expect[key]))
+                                                       ret = req.expect[key];
 
-               list: function(objects)
-               {
-                       var msg = _luci2.rpc._wrap_msg('list', objects);
-                       return _luci2.rpc._post_msg(msg);
-               },
+                                               break;
+                                       }
+                               }
 
-               access: function(scope, object, method, cb)
-               {
-                       return _luci2.rpc._post_single('session', 'access', {
-                               'sid':      _luci2.globals.sid,
-                               'scope':        scope,
-                               'object':   object,
-                               'function': method
-                       }, function(rv) {
-                               return retcb(cb, (rv[0] == 0 && rv[1] && rv[1].access == true));
-                       });
-               }
-       };
+                               /* apply filter */
+                               if (typeof(req.filter) == 'function')
+                               {
+                                       req.priv[0] = ret;
+                                       req.priv[1] = req.params;
+                                       ret = req.filter.apply(_luci2.rpc, req.priv);
+                               }
 
-       this.uci = {
+                               /* store response data */
+                               if (typeof(req.index) == 'number')
+                                       data[req.index] = ret;
+                               else
+                                       data = ret;
 
-               writable: function(cb)
-               {
-                       return _luci2.rpc.access('ubus', 'uci', 'commit', cb);
-               },
+                               /* delete request object */
+                               delete _luci2.rpc._requests[reqs[i].id];
+                       }
 
-               add: function(config, type, cb)
-               {
-                       return rcall('uci', 'add', { config: config, type: type }, 'section', '', cb);
+                       return $.Deferred().resolveWith(this, [ data ]);
                },
 
-               apply: function()
+               list: function()
                {
+                       var params = [ ];
+                       for (var i = 0; i < arguments.length; i++)
+                               params[i] = arguments[i];
 
-               },
+                       var msg = {
+                               jsonrpc: '2.0',
+                               id:      this._id++,
+                               method:  'list',
+                               params:  (params.length > 0) ? params : undefined
+                       };
 
-               changes: function(config)
-               {
-                       return rcall('uci', 'changes', { config: config }, 'changes', [ ], cb);
+                       return this._call(msg, this._list_cb);
                },
 
-               commit: function(config)
+               batch: function()
                {
-                       return rcall('uci', 'commit', { config: config }, undefined, undefined, cb);
+                       if (!$.isArray(this._batch))
+                               this._batch = [ ];
                },
 
-               'delete': function(config, section, option)
+               flush: function()
                {
-                       var req = { config: config, section: section };
+                       if (!$.isArray(this._batch))
+                               return _luci2.deferrable([ ]);
 
-                       if (isa(option, 'Array'))
-                               req.options = option;
-                       else
-                               req.option = option;
+                       var req = this._batch;
+                       delete this._batch;
 
-                       return rcall('uci', 'delete', req, undefined, undefined, cb);
+                       /* call rpc */
+                       return this._call(req, this._call_cb);
                },
 
-               delete_all: function(config, type, matches)
+               declare: function(options)
                {
-                       return rcall('uci', 'delete', { config: config, type: type, match: matches }, undefined, undefined, cb);
-               },
+                       var _rpc = this;
 
-               foreach: function(config, type, cb)
+                       return function() {
+                               /* build parameter object */
+                               var p_off = 0;
+                               var params = { };
+                               if ($.isArray(options.params))
+                                       for (p_off = 0; p_off < options.params.length; p_off++)
+                                               params[options.params[p_off]] = arguments[p_off];
+
+                               /* all remaining arguments are private args */
+                               var priv = [ undefined, undefined ];
+                               for (; p_off < arguments.length; p_off++)
+                                       priv.push(arguments[p_off]);
+
+                               /* store request info */
+                               var req = _rpc._requests[_rpc._id] = {
+                                       expect: options.expect,
+                                       filter: options.filter,
+                                       params: params,
+                                       priv:   priv
+                               };
+
+                               /* build message object */
+                               var msg = {
+                                       jsonrpc: '2.0',
+                                       id:      _rpc._id++,
+                                       method:  'call',
+                                       params:  [
+                                               _luci2.globals.sid,
+                                               options.object,
+                                               options.method,
+                                               params
+                                       ]
+                               };
+
+                               /* when a batch is in progress then store index in request data
+                                * and push message object onto the stack */
+                               if ($.isArray(_rpc._batch))
+                               {
+                                       req.index = _rpc._batch.push(msg) - 1;
+                                       return _luci2.deferrable(msg);
+                               }
+
+                               /* call rpc */
+                               return _rpc._call(msg, _rpc._call_cb);
+                       };
+               }
+       };
+
+       this.UCIContext = Class.extend({
+
+               init: function()
                {
-                       return rcall('uci', 'get', { config: config, type: type }, 'values', { }, function(sections) {
-                               for (var s in sections)
-                                       cb(sections[s]);
-                       });
+                       this.state = {
+                               newid:   0,
+                               values:  { },
+                               creates: { },
+                               changes: { },
+                               deletes: { },
+                               reorder: { }
+                       };
                },
 
-               get: function(config, section, option, cb)
+               _load: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'get',
+                       params: [ 'config' ],
+                       expect: { values: { } }
+               }),
+
+               _order: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'order',
+                       params: [ 'config', 'sections' ]
+               }),
+
+               _add: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'add',
+                       params: [ 'config', 'type', 'name', 'values' ],
+                       expect: { section: '' }
+               }),
+
+               _set: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'set',
+                       params: [ 'config', 'section', 'values' ]
+               }),
+
+               _delete: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'delete',
+                       params: [ 'config', 'section', 'options' ]
+               }),
+
+               load: function(packages)
                {
-                       return rcall('uci', 'get', { config: config, section: section, option: option }, undefined, { }, function(res) {
-                               if (typeof(option) == 'undefined')
-                                       return retcb(cb, (res.values && res.values['.type']) ? res.values['.type'] : undefined);
+                       var self = this;
+                       var seen = { };
+                       var pkgs = [ ];
+
+                       if (!$.isArray(packages))
+                               packages = [ packages ];
+
+                       _luci2.rpc.batch();
+
+                       for (var i = 0; i < packages.length; i++)
+                               if (!seen[packages[i]])
+                               {
+                                       pkgs.push(packages[i]);
+                                       seen[packages[i]] = true;
+                                       self._load(packages[i]);
+                               }
 
-                               return retcb(cb, res.value);
+                       return _luci2.rpc.flush().then(function(responses) {
+                               for (var i = 0; i < responses.length; i++)
+                                       self.state.values[pkgs[i]] = responses[i];
+
+                               return pkgs;
                        });
                },
 
-               get_all: function(config, section, cb)
+               unload: function(packages)
                {
-                       return rcall('uci', 'get', { config: config, section: section }, 'values', { }, cb);
+                       if (!$.isArray(packages))
+                               packages = [ packages ];
+
+                       for (var i = 0; i < packages.length; i++)
+                       {
+                               delete this.state.values[packages[i]];
+                               delete this.state.creates[packages[i]];
+                               delete this.state.changes[packages[i]];
+                               delete this.state.deletes[packages[i]];
+                       }
                },
 
-               get_first: function(config, type, option, cb)
+               add: function(conf, type, name)
                {
-                       return rcall('uci', 'get', { config: config, type: type }, 'values', { }, function(sections) {
-                               for (var s in sections)
-                               {
-                                       var val = (typeof(option) == 'string') ? sections[s][option] : sections[s]['.name'];
+                       var c = this.state.creates;
+                       var s = '.new.%d'.format(this.state.newid++);
 
-                                       if (typeof(val) != 'undefined')
-                                               return retcb(cb, val);
-                               }
+                       if (!c[conf])
+                               c[conf] = { };
 
-                               return retcb(cb, undefined);
-                       });
-               },
+                       c[conf][s] = {
+                               '.type':      type,
+                               '.name':      s,
+                               '.create':    name,
+                               '.anonymous': !name,
+                               '.index':     1000 + this.state.newid
+                       };
 
-               section: function(config, type, name, values, cb)
-               {
-                       return rcall('uci', 'add', { config: config, type: type, name: name, values: values }, 'section', undefined, cb);
+                       return s;
                },
 
-               set: function(config, section, option, value, cb)
+               remove: function(conf, sid)
                {
-                       if (typeof(value) == 'undefined' && typeof(option) == 'string')
-                               return rcall('uci', 'add', { config: config, section: section, type: option }, undefined, undefined, cb);
-                       else if (isa(option, 'Object'))
-                               return rcall('uci', 'set', { config: config, section: section, values: option }, undefined, undefined, cb);
+                       var n = this.state.creates;
+                       var c = this.state.changes;
+                       var d = this.state.deletes;
+
+                       /* requested deletion of a just created section */
+                       if (sid.indexOf('.new.') == 0)
+                       {
+                               if (n[conf])
+                                       delete n[conf][sid];
+                       }
                        else
+                       {
+                               if (c[conf])
+                                       delete c[conf][sid];
 
-                       var values = { };
-                           values[option] = value;
+                               if (!d[conf])
+                                       d[conf] = { };
 
-                       return rcall('uci', 'set', { config: config, section: section, values: values }, undefined, undefined, cb);
+                               d[conf][sid] = true;
+                       }
                },
 
-               order: function(config, sections, cb)
+               sections: function(conf, type, cb)
                {
-                       return rcall('uci', 'order', { config: config, sections: sections }, undefined, undefined, cb);
-               }
-       };
+                       var sa = [ ];
+                       var v = this.state.values[conf];
+                       var n = this.state.creates[conf];
+                       var c = this.state.changes[conf];
+                       var d = this.state.deletes[conf];
 
-       this.network = {
-               getNetworkStatus: function(cb)
-               {
-                       var ifaces = [ ];
-                       var assign = function(target, key)
-                       {
-                               return function(value) {
-                                       if (typeof(value) != 'undefined' && !$.isEmptyObject(value))
-                                               target[key] = value;
-                               };
-                       };
+                       if (!v)
+                               return sa;
 
-                       return _luci2.rpc.list().then(function(data) {
-                               var requests = [ ];
+                       for (var s in v)
+                               if (!d || d[s] !== true)
+                                       if (!type || v[s]['.type'] == type)
+                                               sa.push($.extend({ }, v[s], c ? c[s] : undefined));
 
-                               for (var i = 0; i < data.length; i++)
-                               {
-                                       if (data[i].indexOf('network.interface.') != 0)
-                                               continue;
+                       if (n)
+                               for (var s in n)
+                                       if (!type || n[s]['.type'] == type)
+                                               sa.push(n[s]);
 
-                                       var ifname = data[i].substring(18);
-                                       if (ifname == 'loopback')
-                                               continue;
+                       sa.sort(function(a, b) {
+                               return a['.index'] - b['.index'];
+                       });
 
-                                       var iface = { 'name': ifname };
+                       for (var i = 0; i < sa.length; i++)
+                               sa[i]['.index'] = i;
 
-                                       ifaces.push(iface);
-                                       requests.push(['network.interface', 'status', { 'interface': ifname }, iface]);
-                               }
+                       if (typeof(cb) == 'function')
+                               for (var i = 0; i < sa.length; i++)
+                                       cb.call(this, sa[i], sa[i]['.name']);
 
-                               return _luci2.rpc.call(requests, function(responses) {
-                                       for (var key in responses)
-                                               if (responses[key][0] == 0 && responses[key][1] && responses[key][2])
-                                                       $.extend(responses[key][2], responses[key][1]);
-                               });
-                       }).then(function() {
-                               var requests = [ ];
+                       return sa;
+               },
 
-                               for (var i = 0; i < ifaces.length; i++)
-                               {
-                                       var iface = ifaces[i];
+               get: function(conf, sid, opt)
+               {
+                       var v = this.state.values;
+                       var n = this.state.creates;
+                       var c = this.state.changes;
+                       var d = this.state.deletes;
 
-                                       var dev = iface.l3_device || iface.l2_device;
-                                       if (!dev)
-                                               continue;
+                       if (typeof(sid) == 'undefined')
+                               return undefined;
 
-                                       iface.device = { 'name': dev };
-                                       requests[dev] = ['network.device', 'status', { 'name': dev }, iface.device];
-                               }
+                       /* requested option in a just created section */
+                       if (sid.indexOf('.new.') == 0)
+                       {
+                               if (!n[conf])
+                                       return undefined;
 
-                               return _luci2.rpc.call(requests, function(responses) {
-                                       for (var key in responses)
-                                               if (responses[key][0] == 0 && responses[key][1] && responses[key][2])
-                                                       $.extend(responses[key][2], responses[key][1]);
-                               });
-                       }).then(function() {
-                               var requests = [ ];
+                               if (typeof(opt) == 'undefined')
+                                       return n[conf][sid];
 
-                               for (var i = 0; i < ifaces.length; i++)
-                               {
-                                       var iface = ifaces[i];
-                                       if (!iface.device)
-                                               continue;
+                               return n[conf][sid][opt];
+                       }
 
-                                       var subdevs = iface.device['bridge-members'];
-                                       if (!subdevs)
-                                               continue;
+                       /* requested an option value */
+                       if (typeof(opt) != 'undefined')
+                       {
+                               /* check whether option was deleted */
+                               if (d[conf] && d[conf][sid])
+                               {
+                                       if (d[conf][sid] === true)
+                                               return undefined;
 
-                                       iface.subdevices = [ ];
-                                       for (var j = 0; j < subdevs.length; j++)
-                                       {
-                                               iface.subdevices[j] = { 'name': subdevs[j] };
-                                               requests.push(['network.device', 'status', { 'name': subdevs[j] }, iface.subdevices[j]]);
-                                       }
+                                       for (var i = 0; i < d[conf][sid].length; i++)
+                                               if (d[conf][sid][i] == opt)
+                                                       return undefined;
                                }
 
-                               return _luci2.rpc.call(requests, function(responses) {
-                                       for (var key in responses)
-                                               if (responses[key][0] == 0 && responses[key][1] && responses[key][2])
-                                                       $.extend(responses[key][2], responses[key][1]);
-                               });
-                       }).then(function() {
-                               var requests = [ ];
+                               /* check whether option was changed */
+                               if (c[conf] && c[conf][sid] && typeof(c[conf][sid][opt]) != 'undefined')
+                                       return c[conf][sid][opt];
 
-                               for (var i = 0; i < ifaces.length; i++)
-                               {
-                                       var iface = ifaces[i];
+                               /* return base value */
+                               if (v[conf] && v[conf][sid])
+                                       return v[conf][sid][opt];
 
-                                       if (iface.device)
-                                               requests.push(['iwinfo', 'info', { 'device': iface.device.name }, iface.device]);
+                               return undefined;
+                       }
 
-                                       if (iface.subdevices)
-                                               for (var j = 0; j < iface.subdevices.length; j++)
-                                                       requests.push(['iwinfo', 'info', { 'device': iface.subdevices[j].name }, iface.subdevices[j]]);
-                               }
+                       /* requested an entire section */
+                       if (v[conf])
+                               return v[conf][sid];
 
-                               return _luci2.rpc.call(requests, function(responses) {
-                                       for (var key in responses)
-                                               if (responses[key][0] == 0 && responses[key][1] && responses[key][2])
-                                                       if (!$.isEmptyObject(responses[key][1]))
-                                                               responses[key][2].wireless = responses[key][1];
-                               });
-                       }).then(function() {
-                               ifaces.sort(function(a, b) {
-                                       if (a['interface'] < b['interface'])
-                                               return -1;
-                                       else if (a['interface'] > b['interface'])
-                                               return 1;
-                                       else
-                                               return 0;
-                               });
-                               return retcb(cb, ifaces);
-                       });
+                       return undefined;
                },
 
-               findWanInterfaces: function(cb)
+               set: function(conf, sid, opt, val)
                {
-                       return _luci2.rpc.list().then(function(data) {
-                               var requests = { };
-                               for (var i = 0; i < data.length; i++)
-                               {
-                                       if (data[i].indexOf('network.interface.') == 0)
-                                       {
-                                               var ifname = data[i].substring(18);
-                                               requests[ifname] = ['network.interface', 'status', { 'interface': ifname }];
-                                       }
-                               }
-                               return _luci2.rpc.call(requests);
-                       }).then(function(responses) {
-                               var rv = [ ];
-                               for (var ifname in responses)
-                               {
-                                       var response = responses[ifname];
+                       var n = this.state.creates;
+                       var c = this.state.changes;
+                       var d = this.state.deletes;
 
-                                       if (response[0] != 0 || !response[1] || !response[1].route)
-                                               continue;
+                       if (typeof(sid) == 'undefined' ||
+                           typeof(opt) == 'undefined' ||
+                           opt.charAt(0) == '.')
+                               return;
 
-                                       for (var rn = 0, rt = response[1].route[rn];
-                                                rn < response[1].route.length;
-                                                rn++, rt = response[1].route[rn])
+                       if (sid.indexOf('.new.') == 0)
+                       {
+                               if (n[conf] && n[conf][sid])
+                               {
+                                       if (typeof(val) != 'undefined')
+                                               n[conf][sid][opt] = val;
+                                       else
+                                               delete n[conf][sid][opt];
+                               }
+                       }
+                       else if (typeof(val) != 'undefined')
+                       {
+                               /* do not set within deleted section */
+                               if (d[conf] && d[conf][sid] === true)
+                                       return;
+
+                               if (!c[conf])
+                                       c[conf] = { };
+
+                               if (!c[conf][sid])
+                                       c[conf][sid] = { };
+
+                               /* undelete option */
+                               if (d[conf] && d[conf][sid])
+                                       d[conf][sid] = _luci2.filterArray(d[conf][sid], opt);
+
+                               c[conf][sid][opt] = val;
+                       }
+                       else
+                       {
+                               if (!d[conf])
+                                       d[conf] = { };
+
+                               if (!d[conf][sid])
+                                       d[conf][sid] = [ ];
+
+                               if (d[conf][sid] !== true)
+                                       d[conf][sid].push(opt);
+                       }
+               },
+
+               unset: function(conf, sid, opt)
+               {
+                       return this.set(conf, sid, opt, undefined);
+               },
+
+               _reload: function()
+               {
+                       var pkgs = [ ];
+
+                       for (var pkg in this.state.values)
+                               pkgs.push(pkg);
+
+                       this.init();
+
+                       return this.load(pkgs);
+               },
+
+               _reorder: function()
+               {
+                       var v = this.state.values;
+                       var n = this.state.creates;
+                       var r = this.state.reorder;
+
+                       if ($.isEmptyObject(r))
+                               return _luci2.deferrable();
+
+                       _luci2.rpc.batch();
+
+                       /*
+                        gather all created and existing sections, sort them according
+                        to their index value and issue an uci order call
+                       */
+                       for (var c in r)
+                       {
+                               var o = [ ];
+
+                               if (n && n[c])
+                                       for (var s in n[c])
+                                               o.push(n[c][s]);
+
+                               for (var s in v[c])
+                                       o.push(v[c][s]);
+
+                               if (o.length > 0)
+                               {
+                                       o.sort(function(a, b) {
+                                               return (a['.index'] - b['.index']);
+                                       });
+
+                                       var sids = [ ];
+
+                                       for (var i = 0; i < o.length; i++)
+                                               sids.push(o[i]['.name']);
+
+                                       this._order(c, sids);
+                               }
+                       }
+
+                       this.state.reorder = { };
+                       return _luci2.rpc.flush();
+               },
+
+               swap: function(conf, sid1, sid2)
+               {
+                       var s1 = this.get(conf, sid1);
+                       var s2 = this.get(conf, sid2);
+                       var n1 = s1 ? s1['.index'] : NaN;
+                       var n2 = s2 ? s2['.index'] : NaN;
+
+                       if (isNaN(n1) || isNaN(n2))
+                               return false;
+
+                       s1['.index'] = n2;
+                       s2['.index'] = n1;
+
+                       this.state.reorder[conf] = true;
+
+                       return true;
+               },
+
+               save: function()
+               {
+                       _luci2.rpc.batch();
+
+                       var self = this;
+                       var snew = [ ];
+
+                       if (self.state.creates)
+                               for (var c in self.state.creates)
+                                       for (var s in self.state.creates[c])
                                        {
-                                               if (typeof(rt.table) != 'undefined')
-                                                       continue;
+                                               var r = {
+                                                       config: c,
+                                                       values: { }
+                                               };
 
-                                               if (rt.target == '0.0.0.0' && rt.mask == 0)
-                                                       rv[0] = response[1];
-                                               else if (rt.target == '::' && rt.mask == 0)
-                                                       rv[1] = response[1];
+                                               for (var k in self.state.creates[c][s])
+                                               {
+                                                       if (k == '.type')
+                                                               r.type = self.state.creates[c][s][k];
+                                                       else if (k == '.create')
+                                                               r.name = self.state.creates[c][s][k];
+                                                       else if (k.charAt(0) != '.')
+                                                               r.values[k] = self.state.creates[c][s][k];
+                                               }
+
+                                               snew.push(self.state.creates[c][s]);
+
+                                               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]);
+
+                       if (self.state.deletes)
+                               for (var c in self.state.deletes)
+                                       for (var s in self.state.deletes[c])
+                                       {
+                                               var o = self.state.deletes[c][s];
+                                               self._delete(c, s, (o === true) ? undefined : o);
+                                       }
+
+                       return _luci2.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
+                               */
+                               for (var i = 0; i < snew.length; i++)
+                                       snew[i]['.name'] = responses[i];
+
+                               return self._reorder();
+                       });
+               },
+
+               _apply: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'apply',
+                       params: [ 'timeout', 'rollback' ]
+               }),
+
+               _confirm: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'confirm'
+               }),
+
+               apply: function(timeout)
+               {
+                       var self = this;
+                       var date = new Date();
+                       var deferred = $.Deferred();
+
+                       if (typeof(timeout) != 'number' || timeout < 1)
+                               timeout = 10;
+
+                       self._apply(timeout, true).then(function(rv) {
+                               if (rv != 0)
+                               {
+                                       deferred.rejectWith(self, [ rv ]);
+                                       return;
                                }
 
-                               return retcb(cb, rv);
+                               var try_deadline = date.getTime() + 1000 * timeout;
+                               var try_confirm = function()
+                               {
+                                       return self._confirm().then(function(rv) {
+                                               if (rv != 0)
+                                               {
+                                                       if (date.getTime() < try_deadline)
+                                                               window.setTimeout(try_confirm, 250);
+                                                       else
+                                                               deferred.rejectWith(self, [ rv ]);
+
+                                                       return;
+                                               }
+
+                                               deferred.resolveWith(self, [ rv ]);
+                                       });
+                               };
+
+                               window.setTimeout(try_confirm, 1000);
                        });
+
+                       return deferred;
                },
 
-               getDHCPLeases: function(cb)
+               changes: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'changes',
+                       expect: { changes: { } }
+               }),
+
+               readable: function(conf)
                {
-                       return rcall('luci2.network', 'dhcp_leases', undefined, 'leases', [ ], cb);
+                       return _luci2.session.hasACL('uci', conf, 'read');
                },
 
-               getDHCPv6Leases: function(cb)
+               writable: function(conf)
                {
-                       return rcall('luci2.network', 'dhcp6_leases', undefined, 'leases', [ ], cb);
+                       return _luci2.session.hasACL('uci', conf, 'write');
+               }
+       });
+
+       this.uci = {
+
+               writable: function()
+               {
+                       return _luci2.session.access('ubus', 'uci', 'commit');
                },
 
-               getRoutes: function(cb)
+               add: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'add',
+                       params: [ 'config', 'type', 'name', 'values' ],
+                       expect: { section: '' }
+               }),
+
+               apply: function()
                {
-                       return rcall('luci2.network', 'routes', undefined, 'routes', [ ], cb);
+
                },
 
-               getIPv6Routes: function(cb)
+               configs: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'configs',
+                       expect: { configs: [ ] }
+               }),
+
+               _changes: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'changes',
+                       params: [ 'config' ],
+                       expect: { changes: [ ] }
+               }),
+
+               changes: function(config)
                {
-                       return rcall('luci2.network', 'routes6', undefined, 'routes', [ ], cb);
+                       if (typeof(config) == 'string')
+                               return this._changes(config);
+
+                       var configlist;
+                       return this.configs().then(function(configs) {
+                               _luci2.rpc.batch();
+                               configlist = configs;
+
+                               for (var i = 0; i < configs.length; i++)
+                                       _luci2.uci._changes(configs[i]);
+
+                               return _luci2.rpc.flush();
+                       }).then(function(changes) {
+                               var rv = { };
+
+                               for (var i = 0; i < configlist.length; i++)
+                                       if (changes[i].length)
+                                               rv[configlist[i]] = changes[i];
+
+                               return rv;
+                       });
                },
 
-               getARPTable: function(cb)
+               commit: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'commit',
+                       params: [ 'config' ]
+               }),
+
+               _delete_one: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'delete',
+                       params: [ 'config', 'section', 'option' ]
+               }),
+
+               _delete_multiple: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'delete',
+                       params: [ 'config', 'section', 'options' ]
+               }),
+
+               'delete': function(config, section, option)
                {
-                       return rcall('luci2.network', 'arp_table', undefined, 'entries', [ ], cb);
+                       if ($.isArray(option))
+                               return this._delete_multiple(config, section, option);
+                       else
+                               return this._delete_one(config, section, option);
                },
 
-               getInterfaceStatus: function(iface, cb)
+               delete_all: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'delete',
+                       params: [ 'config', 'type', 'match' ]
+               }),
+
+               _foreach: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'get',
+                       params: [ 'config', 'type' ],
+                       expect: { values: { } }
+               }),
+
+               foreach: function(config, type, cb)
                {
-                       return rcall('network.interface', 'status', { 'interface': iface }, undefined, { }, cb, function(rv) {
-                               rv['interface'] = iface;
-                               rv['l2_device'] = rv['device'];
-                               return rv;
+                       return this._foreach(config, type).then(function(sections) {
+                               for (var s in sections)
+                                       cb(sections[s]);
                        });
                },
 
-               getDeviceStatus: function(dev, cb)
+               get: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'get',
+                       params: [ 'config', 'section', 'option' ],
+                       expect: { '': { } },
+                       filter: function(data, params) {
+                               if (typeof(params.option) == 'undefined')
+                                       return data.values ? data.values['.type'] : undefined;
+                               else
+                                       return data.value;
+                       }
+               }),
+
+               get_all: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'get',
+                       params: [ 'config', 'section' ],
+                       expect: { values: { } },
+                       filter: function(data, params) {
+                               if (typeof(params.section) == 'string')
+                                       data['.section'] = params.section;
+                               else if (typeof(params.config) == 'string')
+                                       data['.package'] = params.config;
+                               return data;
+                       }
+               }),
+
+               get_first: function(config, type, option)
                {
-                       return rcall('network.device', 'status', { name: dev }, undefined, { }, cb, function(rv) {
-                               if (typeof(dev) == 'string')
-                                       rv.device = dev;
-                               return rv;
+                       return this._foreach(config, type).then(function(sections) {
+                               for (var s in sections)
+                               {
+                                       var val = (typeof(option) == 'string') ? sections[s][option] : sections[s]['.name'];
+
+                                       if (typeof(val) != 'undefined')
+                                               return val;
+                               }
+
+                               return undefined;
                        });
                },
 
-               getConntrackCount: function(cb)
+               section: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'add',
+                       params: [ 'config', 'type', 'name', 'values' ],
+                       expect: { section: '' }
+               }),
+
+               _set: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'set',
+                       params: [ 'config', 'section', 'values' ]
+               }),
+
+               set: function(config, section, option, value)
                {
-                       return rcall('luci2.network', 'conntrack_count', undefined, undefined, {
-                               count: 0,
-                               limit: 0
-                       }, cb);
-               }
+                       if (typeof(value) == 'undefined' && typeof(option) == 'string')
+                               return this.section(config, section, option); /* option -> type */
+                       else if ($.isPlainObject(option))
+                               return this._set(config, section, option); /* option -> values */
+
+                       var values = { };
+                           values[option] = value;
+
+                       return this._set(config, section, values);
+               },
+
+               order: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'order',
+                       params: [ 'config', 'sections' ]
+               })
        };
 
-       this.wireless = {
-               getDevices: function(cb) {
-                       return rcall('iwinfo', 'devices', undefined, 'devices', [ ], cb, function(rv) {
-                               rv.sort();
-                               return rv;
+       this.network = {
+               listNetworkNames: function() {
+                       return _luci2.rpc.list('network.interface.*').then(function(list) {
+                               var names = [ ];
+                               for (var name in list)
+                                       if (name != 'network.interface.loopback')
+                                               names.push(name.substring(18));
+                               names.sort();
+                               return names;
                        });
                },
 
-               getInfo: function(dev, cb) {
-                       var parse_info = function(device, info, rv)
-                       {
-                               if (!rv[info.phy])
-                                       rv[info.phy] = {
-                                               networks: [ ]
-                                       };
+               listDeviceNames: _luci2.rpc.declare({
+                       object: 'network.device',
+                       method: 'status',
+                       expect: { '': { } },
+                       filter: function(data) {
+                               var names = [ ];
+                               for (var name in data)
+                                       if (name != 'lo')
+                                               names.push(name);
+                               names.sort();
+                               return names;
+                       }
+               }),
 
-                               var phy = rv[info.phy];
+               getNetworkStatus: function()
+               {
+                       var nets = [ ];
+                       var devs = { };
 
-                               var phy_attrs = [
-                                       'country', 'channel', 'frequency', 'frequency_offset',
-                                       'txpower', 'txpower_offset', 'hwmodes', 'hardware', 'phy'
-                               ];
+                       return this.listNetworkNames().then(function(names) {
+                               _luci2.rpc.batch();
 
-                               var net_attrs = [
-                                       'ssid', 'bssid', 'mode', 'quality', 'quality_max',
-                                       'signal', 'noise', 'bitrate', 'encryption'
-                               ];
+                               for (var i = 0; i < names.length; i++)
+                                       _luci2.network.getInterfaceStatus(names[i]);
+
+                               return _luci2.rpc.flush();
+                       }).then(function(networks) {
+                               for (var i = 0; i < networks.length; i++)
+                               {
+                                       var net = nets[i] = networks[i];
+                                       var dev = net.l3_device || net.l2_device;
+                                       if (dev)
+                                               net.device = devs[dev] || (devs[dev] = { });
+                               }
 
-                               for (var i = 0; i < phy_attrs.length; i++)
-                                       phy[phy_attrs[i]] = info[phy_attrs[i]];
+                               _luci2.rpc.batch();
 
-                               var net = {
-                                       device: device
-                               };
+                               for (var dev in devs)
+                                       _luci2.network.getDeviceStatus(dev);
 
-                               for (var i = 0; i < net_attrs.length; i++)
-                                       net[net_attrs[i]] = info[net_attrs[i]];
+                               return _luci2.rpc.flush();
+                       }).then(function(devices) {
+                               _luci2.rpc.batch();
 
-                               phy.networks.push(net);
+                               for (var i = 0; i < devices.length; i++)
+                               {
+                                       var brm = devices[i]['bridge-members'];
+                                       delete devices[i]['bridge-members'];
 
-                               return phy;
-                       };
+                                       $.extend(devs[devices[i]['device']], devices[i]);
 
-                       if (!dev)
-                       {
-                               return _luci2.wireless.getDevices().then(function(devices) {
-                                       var requests = [ ];
+                                       if (!brm)
+                                               continue;
 
-                                       for (var i = 0; i < devices.length; i++)
+                                       devs[devices[i]['device']].subdevices = [ ];
+
+                                       for (var j = 0; j < brm.length; j++)
                                        {
-                                               if (devices[i].indexOf('.sta') >= 0)
-                                                       continue;
+                                               if (!devs[brm[j]])
+                                               {
+                                                       devs[brm[j]] = { };
+                                                       _luci2.network.getDeviceStatus(brm[j]);
+                                               }
 
-                                               requests[devices[i]] = [ 'iwinfo', 'info', { device: devices[i] } ];
+                                               devs[devices[i]['device']].subdevices[j] = devs[brm[j]];
                                        }
+                               }
 
-                                       return _luci2.rpc.call(requests);
-                               }).then(function(responses) {
-                                       var rv = { };
+                               return _luci2.rpc.flush();
+                       }).then(function(subdevices) {
+                               for (var i = 0; i < subdevices.length; i++)
+                                       $.extend(devs[subdevices[i]['device']], subdevices[i]);
 
-                                       for (var device in responses)
-                                       {
-                                               var response = responses[device];
+                               _luci2.rpc.batch();
 
-                                               if (response[0] != 0 || !response[1])
-                                                       continue;
+                               for (var dev in devs)
+                                       _luci2.wireless.getDeviceStatus(dev);
 
-                                               parse_info(device, response[1], rv);
-                                       }
+                               return _luci2.rpc.flush();
+                       }).then(function(wifidevices) {
+                               for (var i = 0; i < wifidevices.length; i++)
+                                       if (wifidevices[i])
+                                               devs[wifidevices[i]['device']].wireless = wifidevices[i];
 
-                                       return retcb(cb, rv);
+                               nets.sort(function(a, b) {
+                                       if (a['interface'] < b['interface'])
+                                               return -1;
+                                       else if (a['interface'] > b['interface'])
+                                               return 1;
+                                       else
+                                               return 0;
                                });
-                       }
-
-                       return _luci2.rpc.call('iwinfo', 'info', { device: dev }).then(function(response) {
-                               if (response[0] != 0 || !response[1])
-                                       return retcb(cb, { });
 
-                               return retcb(cb, parse_info(dev, response[1], { }));
+                               return nets;
                        });
                },
 
-               getAssocList: function(dev, cb)
+               findWanInterfaces: function(cb)
                {
-                       if (!dev)
-                       {
-                               return _luci2.wireless.getDevices().then(function(devices) {
-                                       var requests = { };
+                       return this.listNetworkNames().then(function(names) {
+                               _luci2.rpc.batch();
 
-                                       for (var i = 0; i < devices.length; i++)
-                                       {
-                                               if (devices[i].indexOf('.sta') >= 0)
-                                                       continue;
+                               for (var i = 0; i < names.length; i++)
+                                       _luci2.network.getInterfaceStatus(names[i]);
 
-                                               requests[devices[i]] = [ 'iwinfo', 'assoclist', { device: devices[i] } ];
-                                       }
+                               return _luci2.rpc.flush();
+                       }).then(function(interfaces) {
+                               var rv = [ undefined, undefined ];
 
-                                       return _luci2.rpc.call(requests);
-                               }).then(function(responses) {
-                                       var rv = [ ];
+                               for (var i = 0; i < interfaces.length; i++)
+                               {
+                                       if (!interfaces[i].route)
+                                               continue;
 
-                                       for (var device in responses)
+                                       for (var j = 0; j < interfaces[i].route.length; j++)
                                        {
-                                               var response = responses[device];
+                                               var rt = interfaces[i].route[j];
 
-                                               if (response[0] != 0 || !response[1] || !response[1].results)
+                                               if (typeof(rt.table) != 'undefined')
                                                        continue;
 
-                                               for (var i = 0; i < response[1].results.length; i++)
-                                               {
-                                                       var station = response[1].results[i];
-
-                                                       station.device = device;
-                                                       rv.push(station);
-                                               }
+                                               if (rt.target == '0.0.0.0' && rt.mask == 0)
+                                                       rv[0] = interfaces[i];
+                                               else if (rt.target == '::' && rt.mask == 0)
+                                                       rv[1] = interfaces[i];
                                        }
+                               }
 
-                                       rv.sort(function(a, b) {
-                                               return (a.device == b.device)
-                                                       ? (a.bssid < b.bssid)
-                                                       : (a.device > b.device)
-                                               ;
-                                       });
+                               return rv;
+                       });
+               },
+
+               getDHCPLeases: _luci2.rpc.declare({
+                       object: 'luci2.network',
+                       method: 'dhcp_leases',
+                       expect: { leases: [ ] }
+               }),
+
+               getDHCPv6Leases: _luci2.rpc.declare({
+                       object: 'luci2.network',
+                       method: 'dhcp6_leases',
+                       expect: { leases: [ ] }
+               }),
+
+               getRoutes: _luci2.rpc.declare({
+                       object: 'luci2.network',
+                       method: 'routes',
+                       expect: { routes: [ ] }
+               }),
+
+               getIPv6Routes: _luci2.rpc.declare({
+                       object: 'luci2.network',
+                       method: 'routes',
+                       expect: { routes: [ ] }
+               }),
+
+               getARPTable: _luci2.rpc.declare({
+                       object: 'luci2.network',
+                       method: 'arp_table',
+                       expect: { entries: [ ] }
+               }),
+
+               getInterfaceStatus: _luci2.rpc.declare({
+                       object: 'network.interface',
+                       method: 'status',
+                       params: [ 'interface' ],
+                       expect: { '': { } },
+                       filter: function(data, params) {
+                               data['interface'] = params['interface'];
+                               data['l2_device'] = data['device'];
+                               delete data['device'];
+                               return data;
+                       }
+               }),
+
+               getDeviceStatus: _luci2.rpc.declare({
+                       object: 'network.device',
+                       method: 'status',
+                       params: [ 'name' ],
+                       expect: { '': { } },
+                       filter: function(data, params) {
+                               data['device'] = params['name'];
+                               return data;
+                       }
+               }),
+
+               getConntrackCount: _luci2.rpc.declare({
+                       object: 'luci2.network',
+                       method: 'conntrack_count',
+                       expect: { '': { count: 0, limit: 0 } }
+               }),
+
+               listSwitchNames: _luci2.rpc.declare({
+                       object: 'luci2.network',
+                       method: 'switch_list',
+                       expect: { switches: [ ] }
+               }),
+
+               getSwitchInfo: _luci2.rpc.declare({
+                       object: 'luci2.network',
+                       method: 'switch_info',
+                       params: [ 'switch' ],
+                       expect: { info: { } },
+                       filter: function(data, params) {
+                               data['attrs']      = data['switch'];
+                               data['vlan_attrs'] = data['vlan'];
+                               data['port_attrs'] = data['port'];
+                               data['switch']     = params['switch'];
+
+                               delete data.vlan;
+                               delete data.port;
+
+                               return data;
+                       }
+               }),
+
+               getSwitchStatus: _luci2.rpc.declare({
+                       object: 'luci2.network',
+                       method: 'switch_status',
+                       params: [ 'switch' ],
+                       expect: { ports: [ ] }
+               }),
+
+
+               runPing: _luci2.rpc.declare({
+                       object: 'luci2.network',
+                       method: 'ping',
+                       params: [ 'data' ],
+                       expect: { '': { code: -1 } }
+               }),
+
+               runPing6: _luci2.rpc.declare({
+                       object: 'luci2.network',
+                       method: 'ping6',
+                       params: [ 'data' ],
+                       expect: { '': { code: -1 } }
+               }),
+
+               runTraceroute: _luci2.rpc.declare({
+                       object: 'luci2.network',
+                       method: 'traceroute',
+                       params: [ 'data' ],
+                       expect: { '': { code: -1 } }
+               }),
+
+               runTraceroute6: _luci2.rpc.declare({
+                       object: 'luci2.network',
+                       method: 'traceroute6',
+                       params: [ 'data' ],
+                       expect: { '': { code: -1 } }
+               }),
+
+               runNslookup: _luci2.rpc.declare({
+                       object: 'luci2.network',
+                       method: 'nslookup',
+                       params: [ 'data' ],
+                       expect: { '': { code: -1 } }
+               }),
+
+
+               setUp: _luci2.rpc.declare({
+                       object: 'luci2.network',
+                       method: 'ifup',
+                       params: [ 'data' ],
+                       expect: { '': { code: -1 } }
+               }),
+
+               setDown: _luci2.rpc.declare({
+                       object: 'luci2.network',
+                       method: 'ifdown',
+                       params: [ 'data' ],
+                       expect: { '': { code: -1 } }
+               })
+       };
+
+       this.wireless = {
+               listDeviceNames: _luci2.rpc.declare({
+                       object: 'iwinfo',
+                       method: 'devices',
+                       expect: { 'devices': [ ] },
+                       filter: function(data) {
+                               data.sort();
+                               return data;
+                       }
+               }),
+
+               getDeviceStatus: _luci2.rpc.declare({
+                       object: 'iwinfo',
+                       method: 'info',
+                       params: [ 'device' ],
+                       expect: { '': { } },
+                       filter: function(data, params) {
+                               if (!$.isEmptyObject(data))
+                               {
+                                       data['device'] = params['device'];
+                                       return data;
+                               }
+                               return undefined;
+                       }
+               }),
+
+               getAssocList: _luci2.rpc.declare({
+                       object: 'iwinfo',
+                       method: 'assoclist',
+                       params: [ 'device' ],
+                       expect: { results: [ ] },
+                       filter: function(data, params) {
+                               for (var i = 0; i < data.length; i++)
+                                       data[i]['device'] = params['device'];
 
-                                       return retcb(cb, rv);
+                               data.sort(function(a, b) {
+                                       if (a.bssid < b.bssid)
+                                               return -1;
+                                       else if (a.bssid > b.bssid)
+                                               return 1;
+                                       else
+                                               return 0;
                                });
+
+                               return data;
                        }
+               }),
 
-                       return _luci2.rpc.call('iwinfo', 'assoclist', { device: dev }).then(function(response) {
-                               var rv = [ ];
+               getWirelessStatus: function() {
+                       return this.listDeviceNames().then(function(names) {
+                               _luci2.rpc.batch();
 
-                               if (response[0] != 0 || !response[1] || !response[1].results)
-                                       return retcb(cb, rv);
+                               for (var i = 0; i < names.length; i++)
+                                       _luci2.wireless.getDeviceStatus(names[i]);
 
-                               for (var i = 0; i < response[1].results.length; i++)
+                               return _luci2.rpc.flush();
+                       }).then(function(networks) {
+                               var rv = { };
+
+                               var phy_attrs = [
+                                       'country', 'channel', 'frequency', 'frequency_offset',
+                                       'txpower', 'txpower_offset', 'hwmodes', 'hardware', 'phy'
+                               ];
+
+                               var net_attrs = [
+                                       'ssid', 'bssid', 'mode', 'quality', 'quality_max',
+                                       'signal', 'noise', 'bitrate', 'encryption'
+                               ];
+
+                               for (var i = 0; i < networks.length; i++)
                                {
-                                       var station = response[1].results[i];
+                                       var phy = rv[networks[i].phy] || (
+                                               rv[networks[i].phy] = { networks: [ ] }
+                                       );
+
+                                       var net = {
+                                               device: networks[i].device
+                                       };
 
-                                       station.device = dev;
-                                       rv.push(station);
+                                       for (var j = 0; j < phy_attrs.length; j++)
+                                               phy[phy_attrs[j]] = networks[i][phy_attrs[j]];
+
+                                       for (var j = 0; j < net_attrs.length; j++)
+                                               net[net_attrs[j]] = networks[i][net_attrs[j]];
+
+                                       phy.networks.push(net);
                                }
 
-                               rv.sort(function(a, b) {
-                                       return (a.bssid < b.bssid);
-                               });
+                               return rv;
+                       });
+               },
+
+               getAssocLists: function()
+               {
+                       return this.listDeviceNames().then(function(names) {
+                               _luci2.rpc.batch();
+
+                               for (var i = 0; i < names.length; i++)
+                                       _luci2.wireless.getAssocList(names[i]);
+
+                               return _luci2.rpc.flush();
+                       }).then(function(assoclists) {
+                               var rv = [ ];
 
-                               return retcb(cb, rv);
+                               for (var i = 0; i < assoclists.length; i++)
+                                       for (var j = 0; j < assoclists[i].length; j++)
+                                               rv.push(assoclists[i][j]);
+
+                               return rv;
                        });
                },
 
@@ -1074,475 +1767,595 @@ function LuCI2()
                }
        };
 
-       this.system = {
-               getInfo: function(cb)
+       this.firewall = {
+               getZoneColor: function(zone)
                {
-                       return _luci2.rpc.call({
-                               info:  [ 'system', 'info',  { } ],
-                               board: [ 'system', 'board', { } ],
-                               disk:  [ 'luci2.system', 'diskfree', { } ]
-                       }).then(function(responses) {
-                               var rv = { };
+                       if ($.isPlainObject(zone))
+                               zone = zone.name;
 
-                               if (responses.info[0] == 0)
-                                       $.extend(rv, responses.info[1]);
+                       if (zone == 'lan')
+                               return '#90f090';
+                       else if (zone == 'wan')
+                               return '#f09090';
 
-                               if (responses.board[0] == 0)
-                                       $.extend(rv, responses.board[1]);
+                       for (var i = 0, hash = 0;
+                                i < zone.length;
+                                hash = zone.charCodeAt(i++) + ((hash << 5) - hash));
 
-                               if (responses.disk[0] == 0)
-                                       $.extend(rv, responses.disk[1]);
+                       for (var i = 0, color = '#';
+                                i < 3;
+                                color += ('00' + ((hash >> i++ * 8) & 0xFF).tozoneing(16)).slice(-2));
 
-                               return retcb(cb, rv);
-                       });
-               },
-
-               getProcessList: function(cb)
-               {
-                       return rcall('luci2.system', 'process_list', undefined, 'processes', [ ], cb, function(rv) {
-                               rv.sort(function(a, b) { return a.pid - b.pid });
-                               return rv;
-                       });
+                       return color;
                },
 
-               getSystemLog: function(cb)
+               findZoneByNetwork: function(network)
                {
-                       return rcall('luci2.system', 'syslog', undefined, 'log', '', cb);
-               },
-
-               getKernelLog: function(cb)
-               {
-                       return rcall('luci2.system', 'dmesg', undefined, 'log', '', cb);
-               },
-
-               getZoneInfo: function(cb)
-               {
-                       return $.getJSON(_luci2.globals.resource + '/zoneinfo.json', cb);
-               },
-
-               canSendSignal: function(cb)
-               {
-                       return _luci2.rpc.access('ubus', 'luci2.system', 'process_signal', cb);
-               },
+                       var self = this;
+                       var zone = undefined;
 
-               sendSignal: function(pid, sig, cb)
-               {
-                       return _luci2.rpc.call('luci2.system', 'process_signal', { pid: pid, signal: sig }).then(function(response) {
-                               return retcb(cb, response[0] == 0);
-                       });
-               },
+                       return _luci2.uci.foreach('firewall', 'zone', function(z) {
+                               if (!z.name || !z.network)
+                                       return;
 
-               initList: function(cb)
-               {
-                       return rcall('luci2.system', 'init_list', undefined, 'initscripts', [ ], cb, function(rv) {
-                               rv.sort(function(a, b) { return (a.start || 0) - (b.start || 0) });
-                               return rv;
-                       });
-               },
+                               if (!$.isArray(z.network))
+                                       z.network = z.network.split(/\s+/);
 
-               initEnabled: function(init, cb)
-               {
-                       return this.initList(function(list) {
-                               for (var i = 0; i < list.length; i++)
-                                       if (list[i].name == init)
-                                               return retcb(cb, !!list[i].enabled);
+                               for (var i = 0; i < z.network.length; i++)
+                               {
+                                       if (z.network[i] == network)
+                                       {
+                                               zone = z;
+                                               break;
+                                       }
+                               }
+                       }).then(function() {
+                               if (zone)
+                                       zone.color = self.getZoneColor(zone);
 
-                               return retcb(cb, false);
+                               return zone;
                        });
-               },
+               }
+       };
 
-               initRun: function(init, action, cb)
-               {
-                       return _luci2.rpc.call('luci2.system', 'init_action', { name: init, action: action }).then(function(response) {
-                               return retcb(cb, response[0] == 0);
-                       });
-               },
+       this.system = {
+               getSystemInfo: _luci2.rpc.declare({
+                       object: 'system',
+                       method: 'info',
+                       expect: { '': { } }
+               }),
+
+               getBoardInfo: _luci2.rpc.declare({
+                       object: 'system',
+                       method: 'board',
+                       expect: { '': { } }
+               }),
+
+               getDiskInfo: _luci2.rpc.declare({
+                       object: 'luci2.system',
+                       method: 'diskfree',
+                       expect: { '': { } }
+               }),
 
-               canInitRun: function(cb)
+               getInfo: function(cb)
                {
-                       return _luci2.rpc.access('ubus', 'luci2.system', 'init_action', cb);
-               },
-
-               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) },
+                       _luci2.rpc.batch();
 
+                       this.getSystemInfo();
+                       this.getBoardInfo();
+                       this.getDiskInfo();
 
-               getRcLocal: function(cb)
-               {
-                       return rcall('luci2.system', 'rclocal_get', undefined, 'data', '', cb);
-               },
+                       return _luci2.rpc.flush().then(function(info) {
+                               var rv = { };
 
-               setRcLocal: function(data, cb)
-               {
-                       return rcall('luci2.system', 'rclocal_set', { data: data }, undefined, undefined, cb);
-               },
+                               $.extend(rv, info[0]);
+                               $.extend(rv, info[1]);
+                               $.extend(rv, info[2]);
 
-               canSetRcLocal: function(cb)
-               {
-                       return _luci2.rpc.access('ubus', 'luci2.system', 'rclocal_set', cb);
+                               return rv;
+                       });
                },
 
+               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;
+                       }
+               }),
 
-               getCrontab: function(cb)
-               {
-                       return rcall('luci2.system', 'crontab_get', undefined, 'data', '', cb);
-               },
+               getSystemLog: _luci2.rpc.declare({
+                       object: 'luci2.system',
+                       method: 'syslog',
+                       expect: { log: '' }
+               }),
 
-               setCrontab: function(data, cb)
-               {
-                       return rcall('luci2.system', 'crontab_set', { data: data }, undefined, undefined, cb);
-               },
+               getKernelLog: _luci2.rpc.declare({
+                       object: 'luci2.system',
+                       method: 'dmesg',
+                       expect: { log: '' }
+               }),
 
-               canSetCrontab: function(cb)
+               getZoneInfo: function(cb)
                {
-                       return _luci2.rpc.access('ubus', 'luci2.system', 'crontab_set', 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({
+                       object: 'luci2.system',
+                       method: 'init_list',
+                       expect: { initscripts: [ ] },
+                       filter: function(data) {
+                               data.sort(function(a, b) { return (a.start || 0) - (b.start || 0) });
+                               return data;
+                       }
+               }),
 
-               getSSHKeys: function(cb)
+               initEnabled: function(init, cb)
                {
-                       return rcall('luci2.system', 'sshkeys_get', undefined, 'keys', [ ], cb);
-               },
+                       return this.initList().then(function(list) {
+                               for (var i = 0; i < list.length; i++)
+                                       if (list[i].name == init)
+                                               return !!list[i].enabled;
 
-               setSSHKeys: function(keys, cb)
-               {
-                       return rcall('luci2.system', 'sshkeys_set', { keys: keys }, undefined, undefined, cb);
+                               return false;
+                       });
                },
 
-               canSetSSHKeys: function(cb)
-               {
-                       return _luci2.rpc.access('ubus', 'luci2.system', 'sshkeys_set', cb);
-               },
+               initRun: _luci2.rpc.declare({
+                       object: 'luci2.system',
+                       method: 'init_action',
+                       params: [ 'name', 'action' ],
+                       filter: function(data) {
+                               return (data == 0);
+                       }
+               }),
 
+               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) },
 
-               setPassword: function(user, pass, cb)
-               {
-                       return rcall('luci2.system', 'password_set', { user: user, password: pass }, undefined, undefined, cb);
-               },
 
-               canSetPassword: function(cb)
-               {
-                       return _luci2.rpc.access('ubus', 'luci2.system', 'password_set', 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' ]
+               }),
 
 
-               listLEDs: function(cb)
-               {
-                       return rcall('luci2.system', 'led_list', undefined, 'leds', [ ], cb);
-               },
+               getCrontab: _luci2.rpc.declare({
+                       object: 'luci2.system',
+                       method: 'crontab_get',
+                       expect: { data: '' }
+               }),
 
-               listUSBDevices: function(cb)
-               {
-                       return rcall('luci2.system', 'usb_list', undefined, 'devices', [ ], cb);
-               },
+               setCrontab: _luci2.rpc.declare({
+                       object: 'luci2.system',
+                       method: 'crontab_set',
+                       params: [ 'data' ]
+               }),
 
 
-               testUpgrade: function(cb)
-               {
-                       return rcall('luci2.system', 'upgrade_test', undefined, undefined, { }, cb);
-               },
+               getSSHKeys: _luci2.rpc.declare({
+                       object: 'luci2.system',
+                       method: 'sshkeys_get',
+                       expect: { keys: [ ] }
+               }),
 
-               startUpgrade: function(keep, cb)
-               {
-                       return rcall('luci2.system', 'upgrade_start', { keep: !!keep }, undefined, undefined, cb);
-               },
+               setSSHKeys: _luci2.rpc.declare({
+                       object: 'luci2.system',
+                       method: 'sshkeys_set',
+                       params: [ 'keys' ]
+               }),
 
-               cleanUpgrade: function(cb)
-               {
-                       return rcall('luci2.system', 'upgrade_clean', undefined, undefined, undefined, cb);
-               },
 
-               canUpgrade: function(cb)
-               {
-                       return _luci2.rpc.access('ubus', 'luci2.system', 'upgrade_start', cb);
-               },
+               setPassword: _luci2.rpc.declare({
+                       object: 'luci2.system',
+                       method: 'password_set',
+                       params: [ 'user', 'password' ]
+               }),
 
 
-               restoreBackup: function(cb)
-               {
-                       return rcall('luci2.system', 'backup_restore', undefined, undefined, undefined, cb);
-               },
+               listLEDs: _luci2.rpc.declare({
+                       object: 'luci2.system',
+                       method: 'led_list',
+                       expect: { leds: [ ] }
+               }),
 
-               cleanBackup: function(cb)
-               {
-                       return rcall('luci2.system', 'backup_clean', undefined, undefined, undefined, cb);
-               },
+               listUSBDevices: _luci2.rpc.declare({
+                       object: 'luci2.system',
+                       method: 'usb_list',
+                       expect: { devices: [ ] }
+               }),
 
-               canRestoreBackup: function(cb)
-               {
-                       return _luci2.rpc.access('ubus', 'luci2.system', 'backup_restore', cb);
-               },
 
+               testUpgrade: _luci2.rpc.declare({
+                       object: 'luci2.system',
+                       method: 'upgrade_test',
+                       expect: { '': { } }
+               }),
 
-               getBackupConfig: function(cb)
-               {
-                       return rcall('luci2.system', 'backup_config_get', undefined, 'config', '', cb);
-               },
+               startUpgrade: _luci2.rpc.declare({
+                       object: 'luci2.system',
+                       method: 'upgrade_start',
+                       params: [ 'keep' ]
+               }),
 
-               setBackupConfig: function(data, cb)
-               {
-                       return rcall('luci2.system', 'backup_config_set', { data: data }, undefined, undefined, cb);
-               },
+               cleanUpgrade: _luci2.rpc.declare({
+                       object: 'luci2.system',
+                       method: 'upgrade_clean'
+               }),
 
-               canSetBackupConfig: function(cb)
-               {
-                       return _luci2.rpc.access('ubus', 'luci2.system', 'backup_config_set', cb);
-               },
 
+               restoreBackup: _luci2.rpc.declare({
+                       object: 'luci2.system',
+                       method: 'backup_restore'
+               }),
 
-               listBackup: function(cb)
-               {
-                       return rcall('luci2.system', 'backup_list', undefined, 'files', [ ], cb);
-               },
+               cleanBackup: _luci2.rpc.declare({
+                       object: 'luci2.system',
+                       method: 'backup_clean'
+               }),
 
 
-               performReboot: function(cb)
-               {
-                       return rcall('luci2.system', 'reboot', undefined, undefined, undefined, cb);
-               },
+               getBackupConfig: _luci2.rpc.declare({
+                       object: 'luci2.system',
+                       method: 'backup_config_get',
+                       expect: { config: '' }
+               }),
 
-               canPerformReboot: function(cb)
-               {
-                       return _luci2.rpc.access('ubus', 'luci2.system', 'reboot', cb);
-               }
+               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({
+                       object: 'luci2.system',
+                       method: 'reboot'
+               })
        };
 
        this.opkg = {
-               updateLists: function(cb)
-               {
-                       return rcall('luci2.opkg', 'update', undefined, undefined, { }, cb);
-               },
-
-               _fetchPackages: function(action, offset, limit, pattern, cb)
+               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 = [ ];
-                       var reqlimit = Math.min(limit, 100);
 
-                       if (reqlimit <= 0)
-                               reqlimit = 100;
+                       return action(offset, limit, pattern).then(function(list) {
+                               if (!list.total || !list.packages)
+                                       return { length: 0, total: 0 };
 
-                       return _luci2.rpc.call('luci2.opkg', action, { offset: offset, limit: reqlimit, pattern: pattern }).then(function(response) {
-                               if (response[0] != 0 || !response[1] || !response[1].total)
-                                       return retcb(cb, { length: 0, total: 0 });
-
-                               packages.push.apply(packages, response[1].packages);
-                               packages.total = response[1].total;
+                               packages.push.apply(packages, list.packages);
+                               packages.total = list.total;
 
                                if (limit <= 0)
-                                       limit = response[1].total;
+                                       limit = list.total;
 
                                if (packages.length >= limit)
-                                       return retcb(cb, packages);
+                                       return packages;
+
+                               _luci2.rpc.batch();
 
-                               var requests = [ ];
                                for (var i = offset + packages.length; i < limit; i += 100)
-                                       requests.push(['luci2.opkg', action, { offset: i, limit: (Math.min(i + 100, limit) % 100) || 100, pattern: pattern }]);
+                                       action(i, (Math.min(i + 100, limit) % 100) || 100, pattern);
 
-                               return _luci2.rpc.call(requests);
-                       }).then(function(responses) {
-                               for (var key in responses)
+                               return _luci2.rpc.flush();
+                       }).then(function(lists) {
+                               for (var i = 0; i < lists.length; i++)
                                {
-                                       var response = responses[key];
-
-                                       if (response[0] != 0 || !response[1] || !response[1].packages)
+                                       if (!lists[i].total || !lists[i].packages)
                                                continue;
 
-                                       packages.push.apply(packages, response[1].packages);
-                                       packages.total = response[1].total;
+                                       packages.push.apply(packages, lists[i].packages);
+                                       packages.total = lists[i].total;
                                }
 
-                               return retcb(cb, packages);
+                               return packages;
                        });
                },
 
-               listPackages: function(offset, limit, pattern, cb)
+               listPackages: function(offset, limit, pattern)
                {
-                       return _luci2.opkg._fetchPackages('list', offset, limit, pattern, cb);
+                       return _luci2.opkg._fetchPackages(_luci2.opkg._allPackages, offset, limit, pattern);
                },
 
-               installedPackages: function(offset, limit, pattern, cb)
+               installedPackages: function(offset, limit, pattern)
                {
-                       return _luci2.opkg._fetchPackages('list_installed', offset, limit, pattern, cb);
+                       return _luci2.opkg._fetchPackages(_luci2.opkg._installedPackages, offset, limit, pattern);
                },
 
-               findPackages: function(offset, limit, pattern, cb)
+               findPackages: function(offset, limit, pattern)
                {
-                       return _luci2.opkg._fetchPackages('find', offset, limit, pattern, cb);
+                       return _luci2.opkg._fetchPackages(_luci2.opkg._findPackages, offset, limit, pattern);
                },
 
-               installPackage: function(name, cb)
-               {
-                       return rcall('luci2.opkg', 'install', { 'package': name }, undefined, { }, cb);
-               },
+               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 = {
 
-               removePackage: function(name, cb)
+               login: _luci2.rpc.declare({
+                       object: 'session',
+                       method: 'login',
+                       params: [ 'username', 'password' ],
+                       expect: { '': { } }
+               }),
+
+               access: _luci2.rpc.declare({
+                       object: 'session',
+                       method: 'access',
+                       params: [ 'scope', 'object', 'function' ],
+                       expect: { access: false }
+               }),
+
+               isAlive: function()
                {
-                       return rcall('luci2.opkg', 'remove', { 'package': name }, undefined, { }, cb);
+                       return _luci2.session.access('ubus', 'session', 'access');
                },
 
-               getConfig: function(cb)
+               startHeartbeat: function()
                {
-                       return rcall('luci2.opkg', 'config_get', undefined, 'config', '', cb);
+                       this._hearbeatInterval = window.setInterval(function() {
+                               _luci2.session.isAlive().then(function(alive) {
+                                       if (!alive)
+                                       {
+                                               _luci2.session.stopHeartbeat();
+                                               _luci2.ui.login(true);
+                                       }
+
+                               });
+                       }, _luci2.globals.timeout * 2);
                },
 
-               setConfig: function(data, cb)
+               stopHeartbeat: function()
                {
-                       return rcall('luci2.opkg', 'config_set', { data: data }, undefined, undefined, cb);
+                       if (typeof(this._hearbeatInterval) != 'undefined')
+                       {
+                               window.clearInterval(this._hearbeatInterval);
+                               delete this._hearbeatInterval;
+                       }
                },
 
-               canInstallPackage: function(cb)
+
+               _acls: { },
+
+               _fetch_acls: _luci2.rpc.declare({
+                       object: 'session',
+                       method: 'access',
+                       expect: { '': { } }
+               }),
+
+               _fetch_acls_cb: function(acls)
                {
-                       return _luci2.rpc.access('ubus', 'luci2.opkg', 'install', cb);
+                       _luci2.session._acls = acls;
                },
 
-               canRemovePackage: function(cb)
+               updateACLs: function()
                {
-                       return _luci2.rpc.access('ubus', 'luci2.opkg', 'remove', cb);
+                       return _luci2.session._fetch_acls()
+                               .then(_luci2.session._fetch_acls_cb);
                },
 
-               canSetConfig: function(cb)
+               hasACL: function(scope, object, func)
                {
-                       return _luci2.rpc.access('ubus', 'luci2.opkg', 'config_set', cb);
+                       var acls = _luci2.session._acls;
+
+                       if (typeof(func) == 'undefined')
+                               return (acls && acls[scope] && acls[scope][object]);
+
+                       if (acls && acls[scope] && acls[scope][object])
+                               for (var i = 0; i < acls[scope][object].length; i++)
+                                       if (acls[scope][object][i] == func)
+                                               return true;
+
+                       return false;
                }
        };
 
        this.ui = {
 
+               saveScrollTop: function()
+               {
+                       this._scroll_top = $(document).scrollTop();
+               },
+
+               restoreScrollTop: function()
+               {
+                       if (typeof(this._scroll_top) == 'undefined')
+                               return;
+
+                       $(document).scrollTop(this._scroll_top);
+
+                       delete this._scroll_top;
+               },
+
                loading: function(enable)
                {
                        var win = $(window);
                        var body = $('body');
-                       var div = _luci2._modal || (
-                               _luci2._modal = $('<div />')
-                                       .addClass('cbi-modal-loader')
-                                       .append($('<div />').text(_luci2.tr('Loading data...')))
+
+                       var state = _luci2.ui._loading || (_luci2.ui._loading = {
+                               modal: $('<div />')
+                                       .addClass('modal fade')
+                                       .append($('<div />')
+                                               .addClass('modal-dialog')
+                                               .append($('<div />')
+                                                       .addClass('modal-content luci2-modal-loader')
+                                                       .append($('<div />')
+                                                               .addClass('modal-body')
+                                                               .text(_luci2.tr('Loading data…')))))
                                        .appendTo(body)
-                       );
+                                       .modal({
+                                               backdrop: 'static',
+                                               keyboard: false
+                                       })
+                       });
 
-                       if (enable)
-                       {
-                               body.css('overflow', 'hidden');
-                               body.css('padding', 0);
-                               body.css('width', win.width());
-                               body.css('height', win.height());
-                               div.css('width', win.width());
-                               div.css('height', win.height());
-                               div.show();
-                       }
-                       else
-                       {
-                               div.hide();
-                               body.css('overflow', '');
-                               body.css('padding', '');
-                               body.css('width', '');
-                               body.css('height', '');
-                       }
+                       state.modal.modal(enable ? 'show' : 'hide');
                },
 
                dialog: function(title, content, options)
                {
                        var win = $(window);
                        var body = $('body');
-                       var div = _luci2._dialog || (
-                               _luci2._dialog = $('<div />')
-                                       .addClass('cbi-modal-dialog')
+
+                       var state = _luci2.ui._dialog || (_luci2.ui._dialog = {
+                               dialog: $('<div />')
+                                       .addClass('modal fade')
                                        .append($('<div />')
+                                               .addClass('modal-dialog')
                                                .append($('<div />')
-                                                       .addClass('cbi-modal-dialog-header'))
-                                               .append($('<div />')
-                                                       .addClass('cbi-modal-dialog-body'))
-                                               .append($('<div />')
-                                                       .addClass('cbi-modal-dialog-footer')
-                                                       .append($('<button />')
-                                                               .addClass('cbi-button')
-                                                               .text(_luci2.tr('Close'))
-                                                               .click(function() {
-                                                                       $('body')
-                                                                               .css('overflow', '')
-                                                                               .css('padding', '')
-                                                                               .css('width', '')
-                                                                               .css('height', '');
-
-                                                                       $(this).parent().parent().parent().hide();
-                                                               }))))
+                                                       .addClass('modal-content')
+                                                       .append($('<div />')
+                                                               .addClass('modal-header')
+                                                               .append('<h4 />')
+                                                                       .addClass('modal-title'))
+                                                       .append($('<div />')
+                                                               .addClass('modal-body'))
+                                                       .append($('<div />')
+                                                               .addClass('modal-footer')
+                                                               .append(_luci2.ui.button(_luci2.tr('Close'), 'primary')
+                                                                       .click(function() {
+                                                                               $(this).parents('div.modal').modal('hide');
+                                                                       })))))
                                        .appendTo(body)
-                       );
+                       });
 
                        if (typeof(options) != 'object')
                                options = { };
 
                        if (title === false)
                        {
-                               body
-                                       .css('overflow', '')
-                                       .css('padding', '')
-                                       .css('width', '')
-                                       .css('height', '');
-
-                               _luci2._dialog.hide();
+                               state.dialog.modal('hide');
 
                                return;
                        }
 
-                       var cnt = div.children().children('div.cbi-modal-dialog-body');
-                       var ftr = div.children().children('div.cbi-modal-dialog-footer');
+                       var cnt = state.dialog.children().children().children('div.modal-body');
+                       var ftr = state.dialog.children().children().children('div.modal-footer');
 
                        ftr.empty();
 
                        if (options.style == 'confirm')
                        {
-                               ftr.append($('<button />')
-                                       .addClass('cbi-button')
-                                       .text(_luci2.tr('Ok'))
+                               ftr.append(_luci2.ui.button(_luci2.tr('Ok'), 'primary')
                                        .click(options.confirm || function() { _luci2.ui.dialog(false) }));
 
-                               ftr.append($('<button />')
-                                       .addClass('cbi-button')
-                                       .text(_luci2.tr('Cancel'))
+                               ftr.append(_luci2.ui.button(_luci2.tr('Cancel'), 'default')
                                        .click(options.cancel || function() { _luci2.ui.dialog(false) }));
                        }
                        else if (options.style == 'close')
                        {
-                               ftr.append($('<button />')
-                                       .addClass('cbi-button')
-                                       .text(_luci2.tr('Close'))
+                               ftr.append(_luci2.ui.button(_luci2.tr('Close'), 'primary')
                                        .click(options.close || function() { _luci2.ui.dialog(false) }));
                        }
                        else if (options.style == 'wait')
                        {
-                               ftr.append($('<button />')
-                                       .addClass('cbi-button')
-                                       .text(_luci2.tr('Close'))
+                               ftr.append(_luci2.ui.button(_luci2.tr('Close'), 'primary')
                                        .attr('disabled', true));
                        }
 
-                       div.find('div.cbi-modal-dialog-header').text(title);
-                       div.show();
-
-                       cnt
-                               .css('max-height', Math.floor(win.height() * 0.70) + 'px')
-                               .empty()
-                               .append(content);
-
-                       div.children()
-                               .css('margin-top', -Math.floor(div.children().height() / 2) + 'px');
+                       state.dialog.find('h4:first').text(title);
+                       state.dialog.modal('show');
 
-                       body.css('overflow', 'hidden');
-                       body.css('padding', 0);
-                       body.css('width', win.width());
-                       body.css('height', win.height());
-                       div.css('width', win.width());
-                       div.css('height', win.height());
+                       cnt.empty().append(content);
                },
 
                upload: function(title, content, options)
                {
-                       var form = _luci2._upload || (
-                               _luci2._upload = $('<form />')
+                       var state = _luci2.ui._upload || (_luci2.ui._upload = {
+                               form: $('<form />')
                                        .attr('method', 'post')
                                        .attr('action', '/cgi-bin/luci-upload')
                                        .attr('enctype', 'multipart/form-data')
@@ -1550,31 +2363,28 @@ function LuCI2()
                                        .append($('<p />'))
                                        .append($('<input />')
                                                .attr('type', 'hidden')
-                                               .attr('name', 'sessionid')
-                                               .attr('value', _luci2.globals.sid))
+                                               .attr('name', 'sessionid'))
                                        .append($('<input />')
                                                .attr('type', 'hidden')
-                                               .attr('name', 'filename')
-                                               .attr('value', options.filename))
+                                               .attr('name', 'filename'))
                                        .append($('<input />')
                                                .attr('type', 'file')
                                                .attr('name', 'filedata')
                                                .addClass('cbi-input-file'))
                                        .append($('<div />')
                                                .css('width', '100%')
-                                               .addClass('progressbar')
-                                               .addClass('intermediate')
+                                               .addClass('progress progress-striped active')
                                                .append($('<div />')
+                                                       .addClass('progress-bar')
                                                        .css('width', '100%')))
                                        .append($('<iframe />')
+                                               .addClass('pull-right')
                                                .attr('name', 'cbi-fileupload-frame')
                                                .css('width', '1px')
                                                .css('height', '1px')
-                                               .css('visibility', 'hidden'))
-                       );
+                                               .css('visibility', 'hidden')),
 
-                       var finish = _luci2._upload_finish_cb || (
-                               _luci2._upload_finish_cb = function(ev) {
+                               finish_cb: function(ev) {
                                        $(this).off('load');
 
                                        var body = (this.contentDocument || this.contentWindow.document).body;
@@ -1599,41 +2409,43 @@ function LuCI2()
                                                        $('<p />').text(_luci2.tr('In case of network problems try uploading the file again.'))
                                                ], { style: 'close' });
                                        }
-                                       else if (typeof(ev.data.cb) == 'function')
+                                       else if (typeof(state.success_cb) == 'function')
                                        {
-                                               ev.data.cb(json);
+                                               state.success_cb(json);
                                        }
-                               }
-                       );
+                               },
 
-                       var confirm = _luci2._upload_confirm_cb || (
-                               _luci2._upload_confirm_cb = function() {
-                                       var d = _luci2._upload;
-                                       var f = d.find('.cbi-input-file');
-                                       var b = d.find('.progressbar');
-                                       var p = d.find('p');
+                               confirm_cb: function() {
+                                       var f = state.form.find('.cbi-input-file');
+                                       var b = state.form.find('.progress');
+                                       var p = state.form.find('p');
 
                                        if (!f.val())
                                                return;
 
-                                       d.find('iframe').on('load', { cb: options.success }, finish);
-                                       d.submit();
+                                       state.form.find('iframe').on('load', state.finish_cb);
+                                       state.form.submit();
 
                                        f.hide();
                                        b.show();
                                        p.text(_luci2.tr('File upload in progress â€¦'));
 
-                                       _luci2._dialog.find('button').prop('disabled', true);
+                                       state.form.parent().parent().find('button').prop('disabled', true);
                                }
-                       );
+                       });
+
+                       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')));
 
-                       _luci2._upload.find('.progressbar').hide();
-                       _luci2._upload.find('.cbi-input-file').val('').show();
-                       _luci2._upload.find('p').text(content || _luci2.tr('Select the file to upload and press "%s" to proceed.').format(_luci2.tr('Ok')));
+                       state.form.find('[name=sessionid]').val(_luci2.globals.sid);
+                       state.form.find('[name=filename]').val(options.filename);
 
-                       _luci2.ui.dialog(title || _luci2.tr('File upload'), _luci2._upload, {
+                       state.success_cb = options.success;
+
+                       _luci2.ui.dialog(title || _luci2.tr('File upload'), state.form, {
                                style: 'confirm',
-                               confirm: confirm
+                               confirm: state.confirm_cb
                        });
                },
 
@@ -1675,8 +2487,8 @@ function LuCI2()
                        //}).then(function() {
                                images.on('load', function() {
                                        var url = this.getAttribute('url');
-                                       _luci2.rpc.access('ubus', 'session', 'access').then(function(response) {
-                                               if (response[0] == 0)
+                                       _luci2.session.isAlive().then(function(access) {
+                                               if (access)
                                                {
                                                        window.clearTimeout(timeout);
                                                        window.clearInterval(interval);
@@ -1711,31 +2523,10 @@ function LuCI2()
 
                login: function(invalid)
                {
-                       if (!_luci2._login_deferred || _luci2._login_deferred.state() != 'pending')
-                               _luci2._login_deferred = $.Deferred();
-
-                       /* try to find sid from hash */
-                       var sid = _luci2.getHash('id');
-                       if (sid && sid.match(/^[a-f0-9]{32}$/))
-                       {
-                               _luci2.globals.sid = sid;
-                               _luci2.rpc.access('ubus', 'session', 'access').then(function(response) {
-                                       if (response[0] == 0)
-                                       {
-                                               _luci2._login_deferred.resolve();
-                                       }
-                                       else
-                                       {
-                                               _luci2.setHash('id', undefined);
-                                               _luci2.ui.login();
-                                       }
-                               });
-
-                               return _luci2._login_deferred;
-                       }
-
-                       var form = _luci2._login || (
-                               _luci2._login = $('<div />')
+                       var state = _luci2.ui._login || (_luci2.ui._login = {
+                               form: $('<form />')
+                                       .attr('target', '')
+                                       .attr('method', 'post')
                                        .append($('<p />')
                                                .addClass('alert-message')
                                                .text(_luci2.tr('Wrong username or password given!')))
@@ -1747,7 +2538,11 @@ function LuCI2()
                                                                .attr('type', 'text')
                                                                .attr('name', 'username')
                                                                .attr('value', 'root')
-                                                               .addClass('cbi-input-text'))))
+                                                               .addClass('form-control')
+                                                               .keypress(function(ev) {
+                                                                       if (ev.which == 10 || ev.which == 13)
+                                                                               state.confirm_cb();
+                                                               }))))
                                        .append($('<p />')
                                                .append($('<label />')
                                                        .text(_luci2.tr('Password'))
@@ -1755,33 +2550,32 @@ function LuCI2()
                                                        .append($('<input />')
                                                                .attr('type', 'password')
                                                                .attr('name', 'password')
-                                                               .addClass('cbi-input-password'))))
+                                                               .addClass('form-control')
+                                                               .keypress(function(ev) {
+                                                                       if (ev.which == 10 || ev.which == 13)
+                                                                               state.confirm_cb();
+                                                               }))))
                                        .append($('<p />')
-                                               .text(_luci2.tr('Enter your username and password above, then click "%s" to proceed.').format(_luci2.tr('Ok'))))
-                       );
+                                               .text(_luci2.tr('Enter your username and password above, then click "%s" to proceed.').format(_luci2.tr('Ok')))),
 
-                       var response = _luci2._login_response_cb || (
-                               _luci2._login_response_cb = function(data) {
-                                       if (typeof(data) == 'object' && typeof(data.sessionid) == 'string')
+                               response_cb: function(response) {
+                                       if (!response.ubus_rpc_session)
                                        {
-                                               _luci2.globals.sid = data.sessionid;
-                                               _luci2.setHash('id', _luci2.globals.sid);
-
-                                               _luci2.ui.dialog(false);
-                                               _luci2._login_deferred.resolve();
+                                               _luci2.ui.login(true);
                                        }
                                        else
                                        {
-                                               _luci2.ui.login(true);
+                                               _luci2.globals.sid = response.ubus_rpc_session;
+                                               _luci2.setHash('id', _luci2.globals.sid);
+                                               _luci2.session.startHeartbeat();
+                                               _luci2.ui.dialog(false);
+                                               state.deferred.resolve();
                                        }
-                               }
-                       );
+                               },
 
-                       var confirm = _luci2._login_confirm_cb || (
-                               _luci2._login_confirm_cb = function() {
-                                       var d = _luci2._login;
-                                       var u = d.find('[name=username]').val();
-                                       var p = d.find('[name=password]').val();
+                               confirm_cb: function() {
+                                       var u = state.form.find('[name=username]').val();
+                                       var p = state.form.find('[name=password]').val();
 
                                        if (!u)
                                                return;
@@ -1798,41 +2592,150 @@ function LuCI2()
                                                ], { style: 'wait' }
                                        );
 
-                                       $.ajax('/cgi-bin/luci-login', {
-                                               type:        'POST',
-                                               cache:       false,
-                                               data:        { username: u, password: p },
-                                               dataType:    'json',
-                                               success:     response,
-                                               error:       response
-                                       });
+                                       _luci2.globals.sid = '00000000000000000000000000000000';
+                                       _luci2.session.login(u, p).then(state.response_cb);
                                }
-                       );
+                       });
+
+                       if (!state.deferred || state.deferred.state() != 'pending')
+                               state.deferred = $.Deferred();
+
+                       /* try to find sid from hash */
+                       var sid = _luci2.getHash('id');
+                       if (sid && sid.match(/^[a-f0-9]{32}$/))
+                       {
+                               _luci2.globals.sid = sid;
+                               _luci2.session.isAlive().then(function(access) {
+                                       if (access)
+                                       {
+                                               _luci2.session.startHeartbeat();
+                                               state.deferred.resolve();
+                                       }
+                                       else
+                                       {
+                                               _luci2.setHash('id', undefined);
+                                               _luci2.ui.login();
+                                       }
+                               });
+
+                               return state.deferred;
+                       }
 
                        if (invalid)
-                               form.find('.alert-message').show();
+                               state.form.find('.alert-message').show();
                        else
-                               form.find('.alert-message').hide();
+                               state.form.find('.alert-message').hide();
 
-                       _luci2.ui.dialog(_luci2.tr('Authorization Required'), form, {
+                       _luci2.ui.dialog(_luci2.tr('Authorization Required'), state.form, {
                                style: 'confirm',
-                               confirm: confirm
+                               confirm: state.confirm_cb
                        });
 
-                       return _luci2._login_deferred;
+                       state.form.find('[name=password]').focus();
+
+                       return state.deferred;
+               },
+
+               cryptPassword: _luci2.rpc.declare({
+                       object: 'luci2.ui',
+                       method: 'crypt',
+                       params: [ 'data' ],
+                       expect: { crypt: '' }
+               }),
+
+
+               _acl_merge_scope: function(acl_scope, scope)
+               {
+                       if ($.isArray(scope))
+                       {
+                               for (var i = 0; i < scope.length; i++)
+                                       acl_scope[scope[i]] = true;
+                       }
+                       else if ($.isPlainObject(scope))
+                       {
+                               for (var object_name in scope)
+                               {
+                                       if (!$.isArray(scope[object_name]))
+                                               continue;
+
+                                       var acl_object = acl_scope[object_name] || (acl_scope[object_name] = { });
+
+                                       for (var i = 0; i < scope[object_name].length; i++)
+                                               acl_object[scope[object_name][i]] = true;
+                               }
+                       }
+               },
+
+               _acl_merge_permission: function(acl_perm, perm)
+               {
+                       if ($.isPlainObject(perm))
+                       {
+                               for (var scope_name in perm)
+                               {
+                                       var acl_scope = acl_perm[scope_name] || (acl_perm[scope_name] = { });
+                                       this._acl_merge_scope(acl_scope, perm[scope_name]);
+                               }
+                       }
+               },
+
+               _acl_merge_group: function(acl_group, group)
+               {
+                       if ($.isPlainObject(group))
+                       {
+                               if (!acl_group.description)
+                                       acl_group.description = group.description;
+
+                               if (group.read)
+                               {
+                                       var acl_perm = acl_group.read || (acl_group.read = { });
+                                       this._acl_merge_permission(acl_perm, group.read);
+                               }
+
+                               if (group.write)
+                               {
+                                       var acl_perm = acl_group.write || (acl_group.write = { });
+                                       this._acl_merge_permission(acl_perm, group.write);
+                               }
+                       }
                },
 
-               renderMainMenu: function()
+               _acl_merge_tree: function(acl_tree, tree)
                {
-                       return rcall('luci2.ui', 'menu', undefined, 'menu', { }, function(entries) {
+                       if ($.isPlainObject(tree))
+                       {
+                               for (var group_name in tree)
+                               {
+                                       var acl_group = acl_tree[group_name] || (acl_tree[group_name] = { });
+                                       this._acl_merge_group(acl_group, tree[group_name]);
+                               }
+                       }
+               },
+
+               listAvailableACLs: _luci2.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]);
+                               return acl_tree;
+                       }
+               }),
+
+               renderMainMenu: _luci2.rpc.declare({
+                       object: 'luci2.ui',
+                       method: 'menu',
+                       expect: { menu: { } },
+                       filter: function(entries) {
                                _luci2.globals.mainMenu = new _luci2.ui.menu();
                                _luci2.globals.mainMenu.entries(entries);
 
                                $('#mainmenu')
                                        .empty()
                                        .append(_luci2.globals.mainMenu.render(0, 1));
-                       });
-               },
+                       }
+               }),
 
                renderViewMenu: function()
                {
@@ -1841,38 +2744,146 @@ function LuCI2()
                                .append(_luci2.globals.mainMenu.render(2, 900));
                },
 
-               renderView: function(node)
+               renderView: function()
+               {
+                       var node = arguments[0];
+                       var name = node.view.split(/\//).join('.');
+                       var args = [ ];
+
+                       for (var i = 1; i < arguments.length; i++)
+                               args.push(arguments[i]);
+
+                       if (_luci2.globals.currentView)
+                               _luci2.globals.currentView.finish();
+
+                       _luci2.ui.renderViewMenu();
+
+                       if (!_luci2._views)
+                               _luci2._views = { };
+
+                       _luci2.setHash('view', node.view);
+
+                       if (_luci2._views[name] instanceof _luci2.ui.view)
+                       {
+                               _luci2.globals.currentView = _luci2._views[name];
+                               return _luci2._views[name].render.apply(_luci2._views[name], args);
+                       }
+
+                       var url = _luci2.globals.resource + '/view/' + name + '.js';
+
+                       return $.ajax(url, {
+                               method: 'GET',
+                               cache: true,
+                               dataType: 'text'
+                       }).then(function(data) {
+                               try {
+                                       var viewConstructorSource = (
+                                               '(function(L, $) { ' +
+                                                       'return %s' +
+                                               '})(_luci2, $);\n\n' +
+                                               '//@ sourceURL=%s'
+                                       ).format(data, url);
+
+                                       var viewConstructor = eval(viewConstructorSource);
+
+                                       _luci2._views[name] = new viewConstructor({
+                                               name: name,
+                                               acls: node.write || { }
+                                       });
+
+                                       _luci2.globals.currentView = _luci2._views[name];
+                                       return _luci2._views[name].render.apply(_luci2._views[name], args);
+                               }
+                               catch(e) {
+                                       alert('Unable to instantiate view "%s": %s'.format(url, e));
+                               };
+
+                               return $.Deferred().resolve();
+                       });
+               },
+
+               updateHostname: function()
+               {
+                       return _luci2.system.getBoardInfo().then(function(info) {
+                               if (info.hostname)
+                                       $('#hostname').text(info.hostname);
+                       });
+               },
+
+               updateChanges: function()
                {
-                       var name = node.view.split(/\//).join('.');
+                       return _luci2.uci.changes().then(function(changes) {
+                               var n = 0;
+                               var html = '';
 
-                       _luci2.ui.renderViewMenu();
+                               for (var config in changes)
+                               {
+                                       var log = [ ];
 
-                       if (!_luci2._views)
-                               _luci2._views = { };
+                                       for (var i = 0; i < changes[config].length; i++)
+                                       {
+                                               var c = changes[config][i];
 
-                       _luci2.setHash('view', node.view);
+                                               switch (c[0])
+                                               {
+                                               case 'order':
+                                                       break;
 
-                       if (_luci2._views[name] instanceof _luci2.ui.view)
-                               return _luci2._views[name].render();
+                                               case 'remove':
+                                                       if (c.length < 3)
+                                                               log.push('uci delete %s.<del>%s</del>'.format(config, c[1]));
+                                                       else
+                                                               log.push('uci delete %s.%s.<del>%s</del>'.format(config, c[1], c[2]));
+                                                       break;
 
-                       return $.ajax(_luci2.globals.resource + '/view/' + name + '.js', {
-                               method: 'GET',
-                               cache: true,
-                               dataType: 'text'
-                       }).then(function(data) {
-                               try {
-                                       var viewConstructor = (new Function(['L', '$'], 'return ' + data))(_luci2, $);
+                                               case 'rename':
+                                                       if (c.length < 4)
+                                                               log.push('uci rename %s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3]));
+                                                       else
+                                                               log.push('uci rename %s.%s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3], c[4]));
+                                                       break;
 
-                                       _luci2._views[name] = new viewConstructor({
-                                               name: name,
-                                               acls: node.write || { }
-                                       });
+                                               case 'add':
+                                                       log.push('uci add %s <ins>%s</ins> (= <ins><strong>%s</strong></ins>)'.format(config, c[2], c[1]));
+                                                       break;
+
+                                               case 'list-add':
+                                                       log.push('uci add_list %s.%s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3], c[4]));
+                                                       break;
+
+                                               case 'list-del':
+                                                       log.push('uci del_list %s.%s.<del>%s=<strong>%s</strong></del>'.format(config, c[1], c[2], c[3], c[4]));
+                                                       break;
+
+                                               case 'set':
+                                                       if (c.length < 4)
+                                                               log.push('uci set %s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2]));
+                                                       else
+                                                               log.push('uci set %s.%s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3], c[4]));
+                                                       break;
+                                               }
+                                       }
 
-                                       return _luci2._views[name].render();
+                                       html += '<code>/etc/config/%s</code><pre class="uci-changes">%s</pre>'.format(config, log.join('\n'));
+                                       n += changes[config].length;
                                }
-                               catch(e) { };
 
-                               return $.Deferred().resolve();
+                               if (n > 0)
+                                       $('#changes')
+                                               .empty()
+                                               .show()
+                                               .append($('<a />')
+                                                       .attr('href', '#')
+                                                       .addClass('label')
+                                                       .addClass('notice')
+                                                       .text(_luci2.trcp('Pending configuration changes', '1 change', '%d changes', n).format(n))
+                                                       .click(function(ev) {
+                                                               _luci2.ui.dialog(_luci2.tr('Staged configuration changes'), html, { style: 'close' });
+                                                               ev.preventDefault();
+                                                       }));
+                               else
+                                       $('#changes')
+                                               .hide();
                        });
                },
 
@@ -1881,33 +2892,67 @@ function LuCI2()
                        _luci2.ui.loading(true);
 
                        $.when(
+                               _luci2.ui.updateHostname(),
+                               _luci2.ui.updateChanges(),
                                _luci2.ui.renderMainMenu()
                        ).then(function() {
                                _luci2.ui.renderView(_luci2.globals.defaultNode).then(function() {
                                        _luci2.ui.loading(false);
                                })
                        });
+               },
+
+               button: function(label, style, title)
+               {
+                       style = style || 'default';
+
+                       return $('<button />')
+                               .attr('type', 'button')
+                               .attr('title', title ? title : '')
+                               .addClass('btn btn-' + style)
+                               .text(label);
                }
        };
 
-       var AbstractWidget = Class.extend({
+       this.ui.AbstractWidget = Class.extend({
                i18n: function(text) {
                        return text;
                },
 
-               toString: function() {
-                       var x = document.createElement('div');
-                               x.appendChild(this.render());
+               label: function() {
+                       var key = arguments[0];
+                       var args = [ ];
+
+                       for (var i = 1; i < arguments.length; i++)
+                               args.push(arguments[i]);
+
+                       switch (typeof(this.options[key]))
+                       {
+                       case 'undefined':
+                               return '';
+
+                       case 'function':
+                               return this.options[key].apply(this, args);
+
+                       default:
+                               return ''.format.apply('' + this.options[key], args);
+                       }
+               },
 
-                       return x.innerHTML;
+               toString: function() {
+                       return $('<div />').append(this.render()).html();
                },
 
                insertInto: function(id) {
                        return $(id).empty().append(this.render());
+               },
+
+               appendTo: function(id) {
+                       return $(id).append(this.render());
                }
        });
 
-       this.ui.view = AbstractWidget.extend({
+       this.ui.view = this.ui.AbstractWidget.extend({
                _fetch_template: function()
                {
                        return $.ajax(_luci2.globals.resource + '/template/' + this.options.name + '.htm', {
@@ -1953,16 +2998,58 @@ function LuCI2()
                                container.append($('<h2 />').append(this.title));
 
                        if (this.description)
-                               container.append($('<div />').addClass('cbi-map-descr').append(this.description));
+                               container.append($('<p />').append(this.description));
 
                        var self = this;
+                       var args = [ ];
+
+                       for (var i = 0; i < arguments.length; i++)
+                               args.push(arguments[i]);
+
                        return this._fetch_template().then(function() {
-                               return _luci2.deferrable(self.execute());
+                               return _luci2.deferrable(self.execute.apply(self, args));
                        });
+               },
+
+               repeat: function(func, interval)
+               {
+                       var self = this;
+
+                       if (!self._timeouts)
+                               self._timeouts = [ ];
+
+                       var index = self._timeouts.length;
+
+                       if (typeof(interval) != 'number')
+                               interval = 5000;
+
+                       var setTimer, runTimer;
+
+                       setTimer = function() {
+                               if (self._timeouts)
+                                       self._timeouts[index] = window.setTimeout(runTimer, interval);
+                       };
+
+                       runTimer = function() {
+                               _luci2.deferrable(func.call(self)).then(setTimer, setTimer);
+                       };
+
+                       runTimer();
+               },
+
+               finish: function()
+               {
+                       if ($.isArray(this._timeouts))
+                       {
+                               for (var i = 0; i < this._timeouts.length; i++)
+                                       window.clearTimeout(this._timeouts[i]);
+
+                               delete this._timeouts;
+                       }
                }
        });
 
-       this.ui.menu = AbstractWidget.extend({
+       this.ui.menu = this.ui.AbstractWidget.extend({
                init: function() {
                        this._nodes = { };
                },
@@ -2012,7 +3099,10 @@ function LuCI2()
                                var child = this.firstChildView(nodes[i]);
                                if (child)
                                {
-                                       $.extend(node, child);
+                                       for (var key in child)
+                                               if (!node.hasOwnProperty(key) && child.hasOwnProperty(key))
+                                                       node[key] = child[key];
+
                                        return node;
                                }
                        }
@@ -2046,9 +3136,9 @@ function LuCI2()
                        var list = $('<ul />');
 
                        if (level == 0)
-                               list.addClass('nav');
+                               list.addClass('nav').addClass('navbar-nav');
                        else if (level == 1)
-                               list.addClass('dropdown-menu');
+                               list.addClass('dropdown-menu').addClass('navbar-inverse');
 
                        for (var i = 0; i < nodes.length; i++)
                        {
@@ -2062,16 +3152,24 @@ function LuCI2()
                                var item = $('<li />')
                                        .append($('<a />')
                                                .attr('href', '#')
-                                               .text(_luci2.tr(nodes[i].title))
-                                               .click(nodes[i], this._onclick))
+                                               .text(_luci2.tr(nodes[i].title)))
                                        .appendTo(list);
 
                                if (nodes[i].childs && level < max)
                                {
                                        item.addClass('dropdown');
-                                       item.find('a').addClass('menu');
+
+                                       item.find('a')
+                                               .addClass('dropdown-toggle')
+                                               .attr('data-toggle', 'dropdown')
+                                               .append('<b class="caret"></b>');
+
                                        item.append(this._render(nodes[i].childs, level + 1));
                                }
+                               else
+                               {
+                                       item.find('a').click(nodes[i], this._onclick);
+                               }
                        }
 
                        return list.get(0);
@@ -2103,7 +3201,7 @@ function LuCI2()
                }
        });
 
-       this.ui.table = AbstractWidget.extend({
+       this.ui.table = this.ui.AbstractWidget.extend({
                init: function()
                {
                        this._rows = [ ];
@@ -2111,11 +3209,11 @@ function LuCI2()
 
                row: function(values)
                {
-                       if (isa(values, 'Array'))
+                       if ($.isArray(values))
                        {
                                this._rows.push(values);
                        }
-                       else if (isa(values, 'Object'))
+                       else if ($.isPlainObject(values))
                        {
                                var v = [ ];
                                for (var i = 0; i < this.options.columns.length; i++)
@@ -2150,7 +3248,7 @@ function LuCI2()
                        }
 
                        var table = document.createElement('table');
-                               table.className = 'cbi-section-table';
+                               table.className = 'table table-condensed table-hover';
 
                        var has_caption = false;
                        var has_description = false;
@@ -2269,38 +3367,42 @@ function LuCI2()
                }
        });
 
-       this.ui.progress = AbstractWidget.extend({
+       this.ui.progress = this.ui.AbstractWidget.extend({
                render: function()
                {
                        var vn = parseInt(this.options.value) || 0;
                        var mn = parseInt(this.options.max) || 100;
                        var pc = Math.floor((100 / mn) * vn);
 
-                       var bar = document.createElement('div');
-                               bar.className = 'progressbar';
-
-                       bar.appendChild(document.createElement('div'));
-                       bar.lastChild.appendChild(document.createElement('div'));
-                       bar.lastChild.style.width = pc + '%';
+                       var text;
 
                        if (typeof(this.options.format) == 'string')
-                               $(bar.lastChild.lastChild).append(this.options.format.format(this.options.value, this.options.max, pc));
+                               text = this.options.format.format(this.options.value, this.options.max, pc);
                        else if (typeof(this.options.format) == 'function')
-                               $(bar.lastChild.lastChild).append(this.options.format(pc));
+                               text = this.options.format(pc);
                        else
-                               $(bar.lastChild.lastChild).append('%.2f%%'.format(pc));
+                               text = '%.2f%%'.format(pc);
 
-                       return bar;
+                       return $('<div />')
+                               .addClass('progress')
+                               .append($('<div />')
+                                       .addClass('progress-bar')
+                                       .addClass('progress-bar-info')
+                                       .css('width', pc + '%'))
+                               .append($('<small />')
+                                       .text(text));
                }
        });
 
-       this.ui.devicebadge = AbstractWidget.extend({
+       this.ui.devicebadge = this.ui.AbstractWidget.extend({
                render: function()
                {
-                       var dev = this.options.l3_device || this.options.device || '?';
+                       var l2dev = this.options.l2_device || this.options.device;
+                       var l3dev = this.options.l3_device;
+                       var dev = l3dev || l2dev || '?';
 
                        var span = document.createElement('span');
-                               span.className = 'ifacebadge';
+                               span.className = 'badge';
 
                        if (typeof(this.options.signal) == 'number' ||
                                typeof(this.options.noise) == 'number')
@@ -2338,7 +3440,7 @@ function LuCI2()
                                var type = 'ethernet';
                                var desc = _luci2.tr('Ethernet device');
 
-                               if (this.options.l3_device != this.options.device)
+                               if (l3dev != l2dev)
                                {
                                        type = 'tunnel';
                                        desc = _luci2.tr('Tunnel interface');
@@ -2432,7 +3534,7 @@ function LuCI2()
                                                                else if (typeof types[label] == 'function')
                                                                {
                                                                        stack.push(types[label]);
-                                                                       stack.push(null);
+                                                                       stack.push([ ]);
                                                                }
                                                                else
                                                                {
@@ -2451,7 +3553,7 @@ function LuCI2()
                                                                throw "Syntax error, argument list follows non-function";
 
                                                        stack[stack.length-1] =
-                                                               arguments.callee(code.substring(pos, i));
+                                                               _luci2.cbi.validation.compile(code.substring(pos, i));
 
                                                        pos = i+1;
                                                }
@@ -2588,6 +3690,7 @@ function LuCI2()
                                }
                        }
 
+                       validation.i18n('Must be a valid IPv6 address');
                        return false;
                },
 
@@ -2860,7 +3963,7 @@ function LuCI2()
        };
 
 
-       var AbstractValue = AbstractWidget.extend({
+       this.cbi.AbstractValue = this.ui.AbstractWidget.extend({
                init: function(name, options)
                {
                        this.name = name;
@@ -2881,27 +3984,43 @@ function LuCI2()
                        return this.section.id('field', sid || '__unknown__', this.name);
                },
 
-               render: function(sid)
+               render: function(sid, condensed)
                {
                        var i = this.instance[sid] = { };
 
-                       i.top = $('<div />').addClass('cbi-value');
+                       i.top = $('<div />');
 
-                       if (typeof(this.options.caption) == 'string')
-                               $('<label />')
-                                       .addClass('cbi-value-title')
-                                       .attr('for', this.id(sid))
-                                       .text(this.options.caption)
-                                       .appendTo(i.top);
+                       if (!condensed)
+                       {
+                               i.top.addClass('form-group');
 
-                       i.widget = $('<div />').addClass('cbi-value-field').append(this.widget(sid)).appendTo(i.top);
-                       i.error = $('<div />').addClass('cbi-value-error').appendTo(i.top);
+                               if (typeof(this.options.caption) == 'string')
+                                       $('<label />')
+                                               .addClass('col-lg-2 control-label')
+                                               .attr('for', this.id(sid))
+                                               .text(this.options.caption)
+                                               .appendTo(i.top);
+                       }
+
+                       i.error = $('<div />')
+                               .hide()
+                               .addClass('label label-danger');
+
+                       i.widget = $('<div />')
+
+                               .append(this.widget(sid))
+                               .append(i.error)
+                               .appendTo(i.top);
+
+                       if (!condensed)
+                       {
+                               i.widget.addClass('col-lg-5');
 
-                       if (typeof(this.options.description) == 'string')
                                $('<div />')
-                                       .addClass('cbi-value-description')
-                                       .text(this.options.description)
+                                       .addClass('col-lg-5')
+                                       .text((typeof(this.options.description) == 'string') ? this.options.description : '')
                                        .appendTo(i.top);
+                       }
 
                        return i.top;
                },
@@ -3005,6 +4124,52 @@ function LuCI2()
                        return chg;
                },
 
+               _ev_validate: function(ev)
+               {
+                       var d = ev.data;
+                       var rv = true;
+                       var val = d.elem.val();
+                       var vstack = d.vstack;
+
+                       if (vstack && typeof(vstack[0]) == 'function')
+                       {
+                               delete validation.message;
+
+                               if ((val.length == 0 && !d.opt))
+                               {
+                                       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();
+                                       rv = false;
+                               }
+                               else if (val.length > 0 && !vstack[0].apply(val, vstack[1]))
+                               {
+                                       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(validation.message.format.apply(validation.message, vstack[1])).show();
+                                       rv = false;
+                               }
+                               else
+                               {
+                                       d.elem.parents('div.form-group, td').first().removeClass('luci2-form-error');
+                                       d.elem.parents('div.input-group, div.form-group, td').first().removeClass('has-error');
+
+                                       if (d.multi && d.inst.widget && d.inst.widget.find('input.error, select.error').length > 0)
+                                               rv = false;
+                                       else
+                                               d.inst.error.text('').hide();
+                               }
+                       }
+
+                       if (rv)
+                               for (var field in d.self.rdependency)
+                                       d.self.rdependency[field].toggle(d.sid);
+
+                       return rv;
+               },
+
                validator: function(sid, elem, multi)
                {
                        if (typeof(this.options.datatype) == 'undefined' && $.isEmptyObject(this.rdependency))
@@ -3029,79 +4194,31 @@ function LuCI2()
                        }
 
                        var evdata = {
-                               self:  this,
-                               sid:   sid,
-                               elem:  elem,
-                               multi: multi,
-                               inst:  this.instance[sid],
-                               opt:   this.options.optional
-                       };
-
-                       var validator = function(ev)
-                       {
-                               var d = ev.data;
-                               var rv = true;
-                               var val = d.elem.val();
-
-                               if (vstack && typeof(vstack[0]) == 'function')
-                               {
-                                       delete validation.message;
-
-                                       if ((val.length == 0 && !d.opt))
-                                       {
-                                               d.elem.addClass('error');
-                                               d.inst.top.addClass('error');
-                                               d.inst.error.text(_luci2.tr('Field must not be empty'));
-                                               rv = false;
-                                       }
-                                       else if (val.length > 0 && !vstack[0].apply(val, vstack[1]))
-                                       {
-                                               d.elem.addClass('error');
-                                               d.inst.top.addClass('error');
-                                               d.inst.error.text(validation.message.format.apply(validation.message, vstack[1]));
-                                               rv = false;
-                                       }
-                                       else
-                                       {
-                                               d.elem.removeClass('error');
-
-                                               if (d.multi && d.inst.widget.find('input.error, select.error').length > 0)
-                                               {
-                                                       rv = false;
-                                               }
-                                               else
-                                               {
-                                                       d.inst.top.removeClass('error');
-                                                       d.inst.error.text('');
-                                               }
-                                       }
-                               }
-
-                               if (rv)
-                               {
-                                       for (var field in d.self.rdependency)
-                                               d.self.rdependency[field].toggle(d.sid);
-                               }
-
-                               return rv;
+                               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, validator);
+                               elem.change(evdata, this._ev_validate);
                        }
                        else if (elem.prop('tagName') == 'INPUT' && elem.attr('type') == 'checkbox')
                        {
-                               elem.click(evdata, validator);
-                               elem.blur(evdata, validator);
+                               elem.click(evdata, this._ev_validate);
+                               elem.blur(evdata, this._ev_validate);
                        }
                        else
                        {
-                               elem.keyup(evdata, validator);
-                               elem.blur(evdata, validator);
+                               elem.keyup(evdata, this._ev_validate);
+                               elem.blur(evdata, this._ev_validate);
                        }
 
-                       elem.attr('cbi-validate', true).on('validate', evdata, validator);
+                       elem.attr('cbi-validate', true).on('validate', evdata, this._ev_validate);
 
                        return elem;
                },
@@ -3126,11 +4243,11 @@ function LuCI2()
                                {
                                        if (typeof(d[i]) == 'string')
                                                dep[d[i]] = true;
-                                       else if (d[i] instanceof AbstractValue)
+                                       else if (d[i] instanceof _luci2.cbi.AbstractValue)
                                                dep[d[i].name] = true;
                                }
                        }
-                       else if (d instanceof AbstractValue)
+                       else if (d instanceof _luci2.cbi.AbstractValue)
                        {
                                dep = { };
                                dep[d.name] = (typeof(v) == 'undefined') ? true : v;
@@ -3238,7 +4355,7 @@ function LuCI2()
                }
        });
 
-       this.cbi.CheckboxValue = AbstractValue.extend({
+       this.cbi.CheckboxValue = this.cbi.AbstractValue.extend({
                widget: function(sid)
                {
                        var o = this.options;
@@ -3251,7 +4368,9 @@ function LuCI2()
                                .attr('type', 'checkbox')
                                .prop('checked', this.ucivalue(sid));
 
-                       return this.validator(sid, i);
+                       return $('<div />')
+                               .addClass('checkbox')
+                               .append(this.validator(sid, i));
                },
 
                ucivalue: function(sid)
@@ -3291,22 +4410,21 @@ function LuCI2()
 
                        if (chg)
                        {
-                               val = val ? this.options.enabled : this.options.disabled;
-
                                if (this.options.optional && val == this.options.initial)
                                        this.map.set(uci.config, uci.section, uci.option, undefined);
                                else
-                                       this.map.set(uci.config, uci.section, uci.option, val);
+                                       this.map.set(uci.config, uci.section, uci.option, val ? this.options.enabled : this.options.disabled);
                        }
 
                        return chg;
                }
        });
 
-       this.cbi.InputValue = AbstractValue.extend({
+       this.cbi.InputValue = this.cbi.AbstractValue.extend({
                widget: function(sid)
                {
                        var i = $('<input />')
+                               .addClass('form-control')
                                .attr('id', this.id(sid))
                                .attr('type', 'text')
                                .attr('placeholder', this.options.placeholder)
@@ -3316,39 +4434,42 @@ function LuCI2()
                }
        });
 
-       this.cbi.PasswordValue = AbstractValue.extend({
+       this.cbi.PasswordValue = this.cbi.AbstractValue.extend({
                widget: function(sid)
                {
                        var i = $('<input />')
+                               .addClass('form-control')
                                .attr('id', this.id(sid))
                                .attr('type', 'password')
                                .attr('placeholder', this.options.placeholder)
                                .val(this.ucivalue(sid));
 
-                       var t = $('<img />')
-                               .attr('src', _luci2.globals.resource + '/icons/cbi/reload.gif')
-                               .attr('title', _luci2.tr('Reveal or hide password'))
-                               .addClass('cbi-button')
-                               .click(function(ev) {
-                                       var i = $(this).prev();
-                                       var t = i.attr('type');
-                                       i.attr('type', (t == 'password') ? 'text' : 'password');
-                                       i = t = null;
-                               });
+                       var t = $('<span />')
+                               .addClass('input-group-btn')
+                               .append(_luci2.ui.button(_luci2.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'));
+                                               i.attr('type', (t == 'password') ? 'text' : 'password');
+                                               b = i = t = null;
+                                       }));
 
                        this.validator(sid, i);
 
                        return $('<div />')
-                               .addClass('cbi-input-password')
+                               .addClass('input-group')
                                .append(i)
                                .append(t);
                }
        });
 
-       this.cbi.ListValue = AbstractValue.extend({
+       this.cbi.ListValue = this.cbi.AbstractValue.extend({
                widget: function(sid)
                {
-                       var s = $('<select />');
+                       var s = $('<select />')
+                               .addClass('form-control');
 
                        if (this.options.optional)
                                $('<option />')
@@ -3395,16 +4516,13 @@ function LuCI2()
                                for (var i = 0; i < this.choices.length; i++)
                                {
                                        $('<label />')
+                                               .addClass('checkbox')
                                                .append($('<input />')
-                                                       .addClass('cbi-input-checkbox')
                                                        .attr('type', 'checkbox')
                                                        .attr('value', this.choices[i][0])
                                                        .prop('checked', s[this.choices[i][0]]))
                                                .append(this.choices[i][1])
                                                .appendTo(t);
-
-                                       $('<br />')
-                                               .appendTo(t);
                                }
 
                        return t;
@@ -3440,7 +4558,7 @@ function LuCI2()
                }
        });
 
-       this.cbi.ComboBox = AbstractValue.extend({
+       this.cbi.ComboBox = this.cbi.AbstractValue.extend({
                _change: function(ev)
                {
                        var s = ev.target;
@@ -3567,7 +4685,7 @@ function LuCI2()
                        var v = s.values || [ ];
                        delete s.values;
 
-                       $(s.parent).children('input').each(function(i) {
+                       $(s.parent).children('div.input-group').children('input').each(function(i) {
                                if (i != del)
                                        v.push(this.value || '');
                        });
@@ -3591,17 +4709,33 @@ function LuCI2()
                                        sid: s.sid,
                                        self: s.self,
                                        parent: s.parent,
-                                       index: i
+                                       index: i,
+                                       remove: ((i+1) < v.length)
                                };
 
+                               var btn;
+                               if (evdata.remove)
+                                       btn = _luci2.ui.button('–', 'danger').click(evdata, this._btnclick);
+                               else
+                                       btn = _luci2.ui.button('+', 'success').click(evdata, this._btnclick);
+
                                if (this.choices)
                                {
                                        var txt = $('<input />')
+                                               .addClass('form-control')
                                                .attr('type', 'text')
-                                               .hide()
-                                               .appendTo(s.parent);
+                                               .hide();
 
                                        var sel = $('<select />')
+                                               .addClass('form-control');
+
+                                       $('<div />')
+                                               .addClass('input-group')
+                                               .append(txt)
+                                               .append(sel)
+                                               .append($('<span />')
+                                                       .addClass('input-group-btn')
+                                                       .append(btn))
                                                .appendTo(s.parent);
 
                                        evdata.input = this.validator(s.sid, txt, true);
@@ -3625,12 +4759,18 @@ function LuCI2()
                                                .attr('type', 'text')
                                                .attr('index', i)
                                                .attr('placeholder', (i == 0) ? this.options.placeholder : '')
-                                               .addClass('cbi-input-text')
+                                               .addClass('form-control')
                                                .keydown(evdata, this._keydown)
                                                .keypress(evdata, this._keypress)
                                                .val(v[i]);
 
-                                       f.appendTo(s.parent);
+                                       $('<div />')
+                                               .addClass('input-group')
+                                               .append(f)
+                                               .append($('<span />')
+                                                       .addClass('input-group-btn')
+                                                       .append(btn))
+                                               .appendTo(s.parent);
 
                                        if (i == focus)
                                        {
@@ -3651,16 +4791,6 @@ function LuCI2()
                                        f = null;
                                }
 
-                               $('<img />')
-                                       .attr('src', _luci2.globals.resource + ((i+1) < v.length ? '/icons/cbi/remove.gif' : '/icons/cbi/add.gif'))
-                                       .attr('title', (i+1) < v.length ? _luci2.tr('Remove entry') : _luci2.tr('Add entry'))
-                                       .addClass('cbi-button')
-                                       .click(evdata, this._btnclick)
-                                       .appendTo(s.parent);
-
-                               $('<br />')
-                                       .appendTo(s.parent);
-
                                evdata = null;
                        }
 
@@ -3725,7 +4855,7 @@ function LuCI2()
 
                                /* arrow up */
                                case 38:
-                                       var prev = input.prevAll('input:first');
+                                       var prev = input.parent().prevAll('div.input-group:first').children('input');
                                        if (prev.is(':visible'))
                                                prev.focus();
                                        else
@@ -3734,7 +4864,7 @@ function LuCI2()
 
                                /* arrow down */
                                case 40:
-                                       var next = input.nextAll('input:first');
+                                       var next = input.parent().nextAll('div.input-group:first').children('input');
                                        if (next.is(':visible'))
                                                next.focus();
                                        else
@@ -3749,7 +4879,7 @@ function LuCI2()
                {
                        if (!this.getAttribute('disabled'))
                        {
-                               if (ev.target.src.indexOf('remove') > -1)
+                               if (ev.data.remove)
                                {
                                        var index = ev.data.index;
                                        ev.data.self._redraw(-index, -1, index, ev.data);
@@ -3809,11 +4939,11 @@ function LuCI2()
                }
        });
 
-       this.cbi.DummyValue = AbstractValue.extend({
+       this.cbi.DummyValue = this.cbi.AbstractValue.extend({
                widget: function(sid)
                {
                        return $('<div />')
-                               .addClass('cbi-value-dummy')
+                               .addClass('form-control-static')
                                .attr('id', this.id(sid))
                                .html(this.ucivalue(sid));
                },
@@ -3824,7 +4954,7 @@ function LuCI2()
                }
        });
 
-       this.cbi.NetworkList = AbstractValue.extend({
+       this.cbi.NetworkList = this.cbi.AbstractValue.extend({
                load: function(sid)
                {
                        var self = this;
@@ -3832,7 +4962,7 @@ function LuCI2()
                        if (!self.interfaces)
                        {
                                self.interfaces = [ ];
-                               return _luci2.network.getNetworkStatus(function(ifaces) {
+                               return _luci2.network.getNetworkStatus().then(function(ifaces) {
                                        self.interfaces = ifaces;
                                        self = null;
                                });
@@ -3861,7 +4991,7 @@ function LuCI2()
                                type = 'wifi';
                                desc = _luci2.tr('Wireless Network');
                        }
-                       else if (dev.name.indexOf('.') > 0)
+                       else if (dev.device.indexOf('.') > 0)
                        {
                                type = 'vlan';
                                desc = _luci2.tr('VLAN interface');
@@ -3869,7 +4999,7 @@ function LuCI2()
 
                        return $('<img />')
                                .attr('src', _luci2.globals.resource + '/icons/' + type + (dev.up ? '' : '_disabled') + '.png')
-                               .attr('title', '%s (%s)'.format(desc, dev.name));
+                               .attr('title', '%s (%s)'.format(desc, dev.device));
                },
 
                widget: function(sid)
@@ -3877,7 +5007,7 @@ function LuCI2()
                        var id = this.id(sid);
                        var ul = $('<ul />')
                                .attr('id', id)
-                               .addClass('cbi-input-networks');
+                               .addClass('list-unstyled');
 
                        var itype = this.options.multiple ? 'checkbox' : 'radio';
                        var value = this.ucivalue(sid);
@@ -3895,12 +5025,12 @@ function LuCI2()
                                {
                                        var iface = this.interfaces[i];
                                        var badge = $('<span />')
-                                               .addClass('ifacebadge')
-                                               .text('%s: '.format(iface.name));
+                                               .addClass('badge')
+                                               .text('%s: '.format(iface['interface']));
 
-                                       if (iface.subdevices)
-                                               for (var j = 0; j < iface.subdevices.length; j++)
-                                                       badge.append(this._device_icon(iface.subdevices[j]));
+                                       if (iface.device && iface.device.subdevices)
+                                               for (var j = 0; j < iface.device.subdevices.length; j++)
+                                                       badge.append(this._device_icon(iface.device.subdevices[j]));
                                        else if (iface.device)
                                                badge.append(this._device_icon(iface.device));
                                        else
@@ -3908,12 +5038,12 @@ function LuCI2()
 
                                        $('<li />')
                                                .append($('<label />')
+                                                       .addClass(itype + ' inline')
                                                        .append($('<input />')
                                                                .attr('name', itype + id)
                                                                .attr('type', itype)
-                                                               .attr('value', iface.name)
-                                                               .prop('checked', !!check[iface.name])
-                                                               .addClass('cbi-input-' + itype))
+                                                               .attr('value', iface['interface'])
+                                                               .prop('checked', !!check[iface['interface']]))
                                                        .append(badge))
                                                .appendTo(ul);
                                }
@@ -3923,12 +5053,12 @@ function LuCI2()
                        {
                                $('<li />')
                                        .append($('<label />')
+                                               .addClass(itype + ' inline text-muted')
                                                .append($('<input />')
                                                        .attr('name', itype + id)
                                                        .attr('type', itype)
                                                        .attr('value', '')
-                                                       .prop('checked', !value)
-                                                       .addClass('cbi-input-' + itype))
+                                                       .prop('checked', !value))
                                                .append(_luci2.tr('unspecified')))
                                        .appendTo(ul);
                        }
@@ -3987,7 +5117,7 @@ function LuCI2()
        });
 
 
-       var AbstractSection = AbstractWidget.extend({
+       this.cbi.AbstractSection = this.ui.AbstractWidget.extend({
                id: function()
                {
                        var s = [ arguments[0], this.map.uci_package, this.uci_type ];
@@ -4037,7 +5167,7 @@ function LuCI2()
 
                        var w = widget ? new widget(name, options) : null;
 
-                       if (!(w instanceof AbstractValue))
+                       if (!(w instanceof _luci2.cbi.AbstractValue))
                                throw 'Widget must be an instance of AbstractValue';
 
                        w.section = this;
@@ -4076,70 +5206,77 @@ function LuCI2()
                        return rv;
                },
 
-               validate: function(sid)
+               validate_section: function(sid)
                {
-                       var rv = true;
-
-                       if (!sid)
-                       {
-                               var as = this.sections();
-                               for (var i = 0; i < as.length; i++)
-                                       if (!this.validate(as[i]['.name']))
-                                               rv = false;
-                               return rv;
-                       }
-
                        var inst = this.instance[sid];
-                       var sv = rv[sid] || (rv[sid] = { });
 
                        var invals = 0;
-                       var legend = $('#' + this.id('sort', sid)).find('legend:first');
-
-                       legend.children('span').detach();
+                       var badge = $('#' + this.id('teaser', sid)).children('span:first');
 
                        for (var i = 0; i < this.tabs.length; i++)
                        {
                                var inval = 0;
-                               var tab = $('#' + this.id('tabhead', sid, this.tabs[i].id));
-
-                               tab.children('span').detach();
+                               var stbadge = $('#' + this.id('nodetab', sid, this.tabs[i].id)).children('span:first');
 
                                for (var j = 0; j < this.tabs[i].fields.length; j++)
                                        if (!this.tabs[i].fields[j].validate(sid))
                                                inval++;
 
                                if (inval > 0)
-                               {
-                                       $('<span />')
-                                               .addClass('badge')
-                                               .attr('title', _luci2.tr('%d Errors'.format(inval)))
+                                       stbadge.show()
                                                .text(inval)
-                                               .appendTo(tab);
+                                               .attr('title', _luci2.trp('1 Error', '%d Errors', inval).format(inval));
+                               else
+                                       stbadge.hide();
 
-                                       invals += inval;
-                                       tab = null;
-                                       rv = false;
-                               }
+                               invals += inval;
                        }
 
                        if (invals > 0)
-                               $('<span />')
-                                       .addClass('badge')
-                                       .attr('title', _luci2.tr('%d Errors'.format(invals)))
+                               badge.show()
                                        .text(invals)
-                                       .appendTo(legend);
+                                       .attr('title', _luci2.trp('1 Error', '%d Errors', invals).format(invals));
+                       else
+                               badge.hide();
 
-                       return rv;
+                       return invals;
+               },
+
+               validate: function()
+               {
+                       this.error_count = 0;
+
+                       var as = this.sections();
+
+                       for (var i = 0; i < as.length; i++)
+                       {
+                               var invals = this.validate_section(as[i]['.name']);
+
+                               if (invals > 0)
+                                       this.error_count += invals;
+                       }
+
+                       var badge = $('#' + this.id('sectiontab')).children('span:first');
+
+                       if (this.error_count > 0)
+                               badge.show()
+                                       .text(this.error_count)
+                                       .attr('title', _luci2.trp('1 Error', '%d Errors', this.error_count).format(this.error_count));
+                       else
+                               badge.hide();
+
+                       return (this.error_count == 0);
                }
        });
 
-       this.cbi.TypedSection = AbstractSection.extend({
+       this.cbi.TypedSection = this.cbi.AbstractSection.extend({
                init: function(uci_type, options)
                {
                        this.uci_type = uci_type;
                        this.options  = options;
                        this.tabs     = [ ];
                        this.fields   = { };
+                       this.error_count  = 0;
                        this.active_panel = 0;
                        this.active_tab   = { };
                },
@@ -4176,7 +5313,7 @@ function LuCI2()
                        this.map.remove(this.map.uci_package, sid);
                },
 
-               _add: function(ev)
+               _ev_add: function(ev)
                {
                        var addb = $(this);
                        var name = undefined;
@@ -4188,25 +5325,33 @@ function LuCI2()
                        if (addb.prop('disabled') || name === '')
                                return;
 
+                       _luci2.ui.saveScrollTop();
+
                        self.active_panel = -1;
                        self.map.save();
                        self.add(name);
                        self.map.redraw();
+
+                       _luci2.ui.restoreScrollTop();
                },
 
-               _remove: function(ev)
+               _ev_remove: function(ev)
                {
                        var self = ev.data.self;
                        var sid  = ev.data.sid;
 
+                       _luci2.ui.saveScrollTop();
+
                        self.map.save();
                        self.remove(sid);
                        self.map.redraw();
 
+                       _luci2.ui.restoreScrollTop();
+
                        ev.stopPropagation();
                },
 
-               _sid: function(ev)
+               _ev_sid: function(ev)
                {
                        var self = ev.data.self;
                        var text = $(this);
@@ -4251,6 +5396,80 @@ function LuCI2()
                        return true;
                },
 
+               _ev_tab: function(ev)
+               {
+                       var self = ev.data.self;
+                       var sid  = ev.data.sid;
+
+                       self.validate();
+                       self.active_tab[sid] = parseInt(ev.target.getAttribute('data-luci2-tab-index'));
+               },
+
+               _ev_panel_collapse: function(ev)
+               {
+                       var self = ev.data.self;
+
+                       var this_panel = $(ev.target);
+                       var this_toggle = this_panel.prevAll('[data-toggle="collapse"]:first');
+
+                       var prev_toggle = $($(ev.delegateTarget).find('[data-toggle="collapse"]:eq(%d)'.format(self.active_panel)));
+                       var prev_panel = $(prev_toggle.attr('data-target'));
+
+                       prev_panel
+                               .removeClass('in')
+                               .addClass('collapse');
+
+                       prev_toggle.find('.luci2-section-teaser')
+                               .show()
+                               .children('span:last')
+                               .empty()
+                               .append(self.teaser(prev_panel.attr('data-luci2-sid')));
+
+                       this_toggle.find('.luci2-section-teaser')
+                               .hide();
+
+                       self.active_panel = parseInt(this_panel.attr('data-luci2-panel-index'));
+                       self.validate();
+               },
+
+               _ev_panel_open: function(ev)
+               {
+                       var self  = ev.data.self;
+                       var panel = $($(this).attr('data-target'));
+                       var index = parseInt(panel.attr('data-luci2-panel-index'));
+
+                       if (index == self.active_panel)
+                               ev.stopPropagation();
+               },
+
+               _ev_sort: function(ev)
+               {
+                       var self    = ev.data.self;
+                       var cur_idx = ev.data.index;
+                       var new_idx = cur_idx + (ev.data.up ? -1 : 1);
+                       var s       = self.sections();
+
+                       if (new_idx >= 0 && new_idx < s.length)
+                       {
+                               var tmp = s[cur_idx]['.index'];
+
+                               s[cur_idx]['.index'] = s[new_idx]['.index'];
+                               s[new_idx]['.index'] = tmp;
+
+                               if (self.active_panel == cur_idx)
+                                       self.active_panel = new_idx;
+                               else if (self.active_panel == new_idx)
+                                       self.active_panel = cur_idx;
+
+                               self.map.uci.reorder = true;
+
+                               self.map.save();
+                               self.map.redraw();
+                       }
+
+                       ev.stopPropagation();
+               },
+
                teaser: function(sid)
                {
                        var tf = this.teaser_fields;
@@ -4264,9 +5483,9 @@ function LuCI2()
                                        for (var i = 0; i < this.options.teasers.length; i++)
                                        {
                                                var f = this.options.teasers[i];
-                                               if (f instanceof AbstractValue)
+                                               if (f instanceof _luci2.cbi.AbstractValue)
                                                        tf.push(f);
-                                               else if (typeof(f) == 'string' && this.fields[f] instanceof AbstractValue)
+                                               else if (typeof(f) == 'string' && this.fields[f] instanceof _luci2.cbi.AbstractValue)
                                                        tf.push(this.fields[f]);
                                        }
                                }
@@ -4299,6 +5518,9 @@ function LuCI2()
 
                _render_add: function()
                {
+                       if (!this.options.addremove)
+                               return null;
+
                        var text = _luci2.tr('Add section');
                        var ttip = _luci2.tr('Create new section...');
 
@@ -4307,7 +5529,7 @@ function LuCI2()
                        else if (typeof(this.options.add_caption) == 'string')
                                text = this.options.add_caption, ttip = '';
 
-                       var add = $('<div />').addClass('cbi-section-add');
+                       var add = $('<div />');
 
                        if (this.options.anonymous === false)
                        {
@@ -4315,15 +5537,15 @@ function LuCI2()
                                        .addClass('cbi-input-text')
                                        .attr('type', 'text')
                                        .attr('placeholder', ttip)
-                                       .blur({ self: this }, this._sid)
-                                       .keyup({ self: this }, this._sid)
+                                       .blur({ self: this }, this._ev_sid)
+                                       .keyup({ self: this }, this._ev_sid)
                                        .appendTo(add);
 
                                $('<img />')
                                        .attr('src', _luci2.globals.resource + '/icons/cbi/add.gif')
                                        .attr('title', text)
                                        .addClass('cbi-button')
-                                       .click({ self: this }, this._add)
+                                       .click({ self: this }, this._ev_add)
                                        .appendTo(add);
 
                                $('<div />')
@@ -4333,20 +5555,19 @@ function LuCI2()
                        }
                        else
                        {
-                               $('<input />')
-                                       .attr('type', 'button')
-                                       .addClass('cbi-button')
-                                       .addClass('cbi-button-add')
-                                       .val(text).attr('title', ttip)
-                                       .click({ self: this }, this._add)
-                                       .appendTo(add)
+                               _luci2.ui.button(text, 'success', ttip)
+                                       .click({ self: this }, this._ev_add)
+                                       .appendTo(add);
                        }
 
                        return add;
                },
 
-               _render_remove: function(sid)
+               _render_remove: function(sid, index)
                {
+                       if (!this.options.addremove)
+                               return null;
+
                        var text = _luci2.tr('Remove');
                        var ttip = _luci2.tr('Remove this section');
 
@@ -4355,228 +5576,236 @@ function LuCI2()
                        else if (typeof(this.options.remove_caption) == 'string')
                                text = this.options.remove_caption, ttip = '';
 
-                       return $('<input />')
-                               .attr('type', 'button')
-                               .addClass('cbi-button')
-                               .addClass('cbi-button-remove')
-                               .val(text).attr('title', ttip)
-                               .click({ self: this, sid: sid }, this._remove);
+                       return _luci2.ui.button(text, 'danger', ttip)
+                               .click({ self: this, sid: sid, index: index }, this._ev_remove);
                },
 
-               _render_caption: function(sid)
+               _render_sort: function(sid, index)
                {
-                       if (typeof(this.options.caption) == 'string')
-                       {
-                               return $('<legend />')
-                                       .text(this.options.caption.format(sid));
-                       }
-                       else if (typeof(this.options.caption) == 'function')
-                       {
-                               return $('<legend />')
-                                       .text(this.options.caption.call(this, sid));
-                       }
+                       if (!this.options.sortable)
+                               return null;
+
+                       var b1 = _luci2.ui.button('↑', 'info', _luci2.tr('Move up'))
+                               .click({ self: this, index: index, up: true }, this._ev_sort);
 
-                       return '';
+                       var b2 = _luci2.ui.button('↓', 'info', _luci2.tr('Move down'))
+                               .click({ self: this, index: index, up: false }, this._ev_sort);
+
+                       return b1.add(b2);
                },
 
-               render: function()
+               _render_caption: function()
                {
-                       var allsections = $();
-                       var panel_index = 0;
+                       return $('<h3 />')
+                               .addClass('panel-title')
+                               .append(this.label('caption') || this.uci_type);
+               },
 
-                       this.instance = { };
+               _render_description: function()
+               {
+                       var text = this.label('description');
 
-                       var s = this.sections();
+                       if (text)
+                               return $('<div />')
+                                       .addClass('luci2-section-description')
+                                       .text(text);
+
+                       return null;
+               },
 
-                       if (s.length == 0)
+               _render_teaser: function(sid, index)
+               {
+                       if (this.options.collabsible || this.map.options.collabsible)
                        {
-                               var fieldset = $('<fieldset />')
-                                       .addClass('cbi-section');
+                               return $('<div />')
+                                       .attr('id', this.id('teaser', sid))
+                                       .addClass('luci2-section-teaser well well-sm')
+                                       .append($('<span />')
+                                               .addClass('badge'))
+                                       .append($('<span />'));
+                       }
 
-                               var head = $('<div />')
-                                       .addClass('cbi-section-head')
-                                       .appendTo(fieldset);
+                       return null;
+               },
 
-                               head.append(this._render_caption(undefined));
+               _render_head: function(condensed)
+               {
+                       if (condensed)
+                               return null;
 
-                               if (typeof(this.options.description) == 'string')
-                               {
-                                       $('<div />')
-                                               .addClass('cbi-section-descr')
-                                               .text(this.options.description)
-                                               .appendTo(head);
-                               }
+                       return $('<div />')
+                               .addClass('panel-heading')
+                               .append(this._render_caption())
+                               .append(this._render_description());
+               },
 
-                               allsections = allsections.add(fieldset);
-                       }
+               _render_tab_description: function(sid, index, tab_index)
+               {
+                       var tab = this.tabs[tab_index];
 
-                       for (var i = 0; i < s.length; i++)
+                       if (typeof(tab.description) == 'string')
                        {
-                               var sid = s[i]['.name'];
-                               var inst = this.instance[sid] = { tabs: [ ] };
-
-                               var fieldset = $('<fieldset />')
-                                       .attr('id', this.id('sort', sid))
-                                       .addClass('cbi-section');
+                               return $('<div />')
+                                       .addClass('cbi-tab-descr')
+                                       .text(tab.description);
+                       }
 
-                               var head = $('<div />')
-                                       .addClass('cbi-section-head')
-                                       .attr('cbi-section-num', this.index)
-                                       .attr('cbi-section-id', sid);
+                       return null;
+               },
 
-                               head.append(this._render_caption(sid));
+               _render_tab_head: function(sid, index, tab_index)
+               {
+                       var tab = this.tabs[tab_index];
+                       var cur = this.active_tab[sid] || 0;
 
-                               if (typeof(this.options.description) == 'string')
-                               {
-                                       $('<div />')
-                                               .addClass('cbi-section-descr')
-                                               .text(this.options.description)
-                                               .appendTo(head);
-                               }
+                       var tabh = $('<li />')
+                               .append($('<a />')
+                                       .attr('id', this.id('nodetab', sid, tab.id))
+                                       .attr('href', '#' + this.id('node', sid, tab.id))
+                                       .attr('data-toggle', 'tab')
+                                       .attr('data-luci2-tab-index', tab_index)
+                                       .text((tab.caption ? tab.caption.format(tab.id) : tab.id) + ' ')
+                                       .append($('<span />')
+                                               .addClass('badge'))
+                                       .on('shown.bs.tab', { self: this, sid: sid }, this._ev_tab));
 
-                               var teaser;
-                               if ((s.length > 1 && this.options.collabsible) || this.map.options.collabsible)
-                                       teaser = $('<div />')
-                                               .addClass('cbi-section-teaser')
-                                               .appendTo(head);
+                       if (cur == tab_index)
+                               tabh.addClass('active');
 
-                               if (this.options.addremove)
-                                       $('<div />')
-                                               .addClass('cbi-section-remove')
-                                               .addClass('right')
-                                               .append(this._render_remove(sid))
-                                               .appendTo(head);
+                       return tabh;
+               },
 
-                               var body = $('<div />')
-                                       .attr('index', panel_index++);
+               _render_tab_body: function(sid, index, tab_index)
+               {
+                       var tab = this.tabs[tab_index];
+                       var cur = this.active_tab[sid] || 0;
 
-                               var fields = $('<fieldset />')
-                                       .addClass('cbi-section-node');
+                       var tabb = $('<div />')
+                               .addClass('tab-pane')
+                               .attr('id', this.id('node', sid, tab.id))
+                               .attr('data-luci2-tab-index', tab_index)
+                               .append(this._render_tab_description(sid, index, tab_index));
 
-                               if (this.tabs.length > 1)
-                               {
-                                       var menu = $('<ul />')
-                                               .addClass('cbi-tabmenu');
+                       if (cur == tab_index)
+                               tabb.addClass('active');
 
-                                       for (var j = 0; j < this.tabs.length; j++)
-                                       {
-                                               var tabid = this.id('tab', sid, this.tabs[j].id);
-                                               var theadid = this.id('tabhead', sid, this.tabs[j].id);
+                       for (var i = 0; i < tab.fields.length; i++)
+                               tabb.append(tab.fields[i].render(sid));
 
-                                               var tabc = $('<div />')
-                                                       .addClass('cbi-tabcontainer')
-                                                       .attr('id', tabid)
-                                                       .attr('index', j);
+                       return tabb;
+               },
 
-                                               if (typeof(this.tabs[j].description) == 'string')
-                                               {
-                                                       $('<div />')
-                                                               .addClass('cbi-tab-descr')
-                                                               .text(this.tabs[j].description)
-                                                               .appendTo(tabc);
-                                               }
+               _render_section_head: function(sid, index)
+               {
+                       var head = $('<div />')
+                               .addClass('luci2-section-header')
+                               .append(this._render_teaser(sid, index))
+                               .append($('<div />')
+                                       .addClass('btn-group')
+                                       .append(this._render_sort(sid, index))
+                                       .append(this._render_remove(sid, index)));
 
-                                               for (var k = 0; k < this.tabs[j].fields.length; k++)
-                                                       this.tabs[j].fields[k].render(sid).appendTo(tabc);
+                       if (this.options.collabsible)
+                       {
+                               head.attr('data-toggle', 'collapse')
+                                       .attr('data-parent', this.id('sectiongroup'))
+                                       .attr('data-target', '#' + this.id('panel', sid))
+                                       .on('click', { self: this }, this._ev_panel_open);
+                       }
 
-                                               tabc.appendTo(fields);
-                                               tabc = null;
+                       return head;
+               },
 
-                                               $('<li />').attr('id', theadid).append(
-                                                       $('<a />')
-                                                               .text(this.tabs[j].caption.format(this.tabs[j].id))
-                                                               .attr('href', '#' + tabid)
-                                               ).appendTo(menu);
-                                       }
+               _render_section_body: function(sid, index)
+               {
+                       var body = $('<div />')
+                               .attr('id', this.id('panel', sid))
+                               .attr('data-luci2-panel-index', index)
+                               .attr('data-luci2-sid', sid);
 
-                                       menu.appendTo(body);
-                                       menu = null;
+                       if (this.options.collabsible || this.map.options.collabsible)
+                       {
+                               body.addClass('panel-collapse collapse');
 
-                                       fields.appendTo(body);
-                                       fields = null;
+                               if (index == this.active_panel)
+                                       body.addClass('in');
+                       }
 
-                                       var t = body.tabs({ active: this.active_tab[sid] });
+                       var tab_heads = $('<ul />')
+                               .addClass('nav nav-tabs');
 
-                                       t.on('tabsactivate', { self: this, sid: sid }, function(ev, ui) {
-                                               var d = ev.data;
-                                               d.self.validate();
-                                               d.self.active_tab[d.sid] = parseInt(ui.newPanel.attr('index'));
-                                       });
-                               }
-                               else
-                               {
-                                       for (var j = 0; j < this.tabs[0].fields.length; j++)
-                                               this.tabs[0].fields[j].render(sid).appendTo(fields);
+                       var tab_bodies = $('<div />')
+                               .addClass('form-horizontal tab-content')
+                               .append(tab_heads);
 
-                                       fields.appendTo(body);
-                                       fields = null;
-                               }
+                       for (var j = 0; j < this.tabs.length; j++)
+                       {
+                               tab_heads.append(this._render_tab_head(sid, index, j));
+                               tab_bodies.append(this._render_tab_body(sid, index, j));
+                       }
 
-                               head.appendTo(fieldset);
-                               head = null;
+                       body.append(tab_bodies);
 
-                               body.appendTo(fieldset);
-                               body = null;
+                       if (this.tabs.length <= 1)
+                               tab_heads.hide();
 
-                               allsections = allsections.add(fieldset);
-                               fieldset = null;
+                       return body;
+               },
 
-                               //this.validate(sid);
-                               //
-                               //if (teaser)
-                               //      teaser.append(this.teaser(sid));
-                       }
+               _render_body: function(condensed)
+               {
+                       var s = this.sections();
 
-                       if (this.options.collabsible && s.length > 1)
-                       {
-                               var a = $('<div />').append(allsections).accordion({
-                                       header: '> fieldset > div.cbi-section-head',
-                                       heightStyle: 'content',
-                                       active: this.active_panel
-                               });
+                       if (this.active_panel < 0)
+                               this.active_panel += s.length;
+                       else if (this.active_panel >= s.length)
+                               this.active_panel = s.length - 1;
 
-                               a.on('accordionbeforeactivate', { self: this }, function(ev, ui) {
-                                       var h = ui.oldHeader;
-                                       var s = ev.data.self;
-                                       var i = h.attr('cbi-section-id');
+                       var body = $('<ul />')
+                               .addClass('list-group');
 
-                                       h.children('.cbi-section-teaser').empty().append(s.teaser(i));
-                                       s.validate();
-                               });
+                       if (this.options.collabsible)
+                       {
+                               body.attr('id', this.id('sectiongroup'))
+                                       .on('show.bs.collapse', { self: this }, this._ev_panel_collapse);
+                       }
 
-                               a.on('accordionactivate', { self: this }, function(ev, ui) {
-                                       ev.data.self.active_panel = parseInt(ui.newPanel.attr('index'));
-                               });
+                       if (s.length == 0)
+                       {
+                               body.append($('<li />')
+                                       .addClass('list-group-item text-muted')
+                                       .text(this.label('placeholder') || _luci2.tr('There are no entries defined yet.')))
+                       }
 
-                               if (this.options.sortable)
-                               {
-                                       var s = a.sortable({
-                                               axis: 'y',
-                                               handle: 'div.cbi-section-head'
-                                       });
+                       for (var i = 0; i < s.length; i++)
+                       {
+                               var sid = s[i]['.name'];
+                               var inst = this.instance[sid] = { tabs: [ ] };
 
-                                       s.on('sortupdate', { self: this, ids: s.sortable('toArray') }, function(ev, ui) {
-                                               var sections = [ ];
-                                               for (var i = 0; i < ev.data.ids.length; i++)
-                                                       sections.push(ev.data.ids[i].substring(ev.data.ids[i].lastIndexOf('.') + 1));
-                                               _luci2.uci.order(ev.data.self.map.uci_package, sections);
-                                       });
+                               body.append($('<li />')
+                                       .addClass('list-group-item')
+                                       .append(this._render_section_head(sid, i))
+                                       .append(this._render_section_body(sid, i)));
+                       }
 
-                                       s.on('sortstop', function(ev, ui) {
-                                               ui.item.children('div.cbi-section-head').triggerHandler('focusout');
-                                       });
-                               }
+                       return body;
+               },
 
-                               if (this.options.addremove)
-                                       this._render_add().appendTo(a);
+               render: function(condensed)
+               {
+                       this.instance = { };
 
-                               return a;
-                       }
+                       var panel = $('<div />')
+                               .addClass('panel panel-default')
+                               .append(this._render_head(condensed))
+                               .append(this._render_body(condensed));
 
                        if (this.options.addremove)
-                               allsections = allsections.add(this._render_add());
+                               panel.append($('<div />')
+                                       .addClass('panel-footer')
+                                       .append(this._render_add()));
 
-                       return allsections;
+                       return panel;
                },
 
                finish: function()
@@ -4587,146 +5816,101 @@ function LuCI2()
                        {
                                var sid = s[i]['.name'];
 
-                               this.validate(sid);
+                               this.validate_section(sid);
 
-                               $('#' + this.id('sort', sid))
-                                       .children('.cbi-section-head')
-                                       .children('.cbi-section-teaser')
-                                       .append(this.teaser(sid));
+                               if (i != this.active_panel)
+                                       $('#' + this.id('teaser', sid)).children('span:last')
+                                               .append(this.teaser(sid));
+                               else
+                                       $('#' + this.id('teaser', sid))
+                                               .hide();
                        }
                }
        });
 
        this.cbi.TableSection = this.cbi.TypedSection.extend({
-               render: function()
+               _render_table_head: function()
                {
-                       var allsections = $();
-                       var panel_index = 0;
-
-                       this.instance = { };
-
-                       var s = this.sections();
-
-                       var fieldset = $('<fieldset />')
-                               .addClass('cbi-section');
-
-                       fieldset.append(this._render_caption(sid));
-
-                       if (typeof(this.options.description) == 'string')
-                       {
-                               $('<div />')
-                                       .addClass('cbi-section-descr')
-                                       .text(this.options.description)
-                                       .appendTo(fieldset);
-                       }
-
-                       var fields = $('<div />')
-                               .addClass('cbi-section-node')
-                               .appendTo(fieldset);
-
-                       var table = $('<table />')
-                               .addClass('cbi-section-table')
-                               .appendTo(fields);
-
                        var thead = $('<thead />')
-                               .append($('<tr />').addClass('cbi-section-table-titles'))
-                               .appendTo(table);
+                               .append($('<tr />')
+                                       .addClass('cbi-section-table-titles'));
 
                        for (var j = 0; j < this.tabs[0].fields.length; j++)
-                               $('<th />')
+                               thead.children().append($('<th />')
                                        .addClass('cbi-section-table-cell')
                                        .css('width', this.tabs[0].fields[j].options.width || '')
-                                       .append(this.tabs[0].fields[j].options.caption)
-                                       .appendTo(thead.children());
+                                       .append(this.tabs[0].fields[j].label('caption')));
 
-                       if (this.options.sortable)
-                               $('<th />').addClass('cbi-section-table-cell').text(' ').appendTo(thead.children());
+                       if (this.options.addremove !== false || this.options.sortable)
+                               thead.children().append($('<th />')
+                                       .addClass('cbi-section-table-cell')
+                                       .text(' '));
 
-                       if (this.options.addremove !== false)
-                               $('<th />').addClass('cbi-section-table-cell').text(' ').appendTo(thead.children());
+                       return thead;
+               },
 
-                       var tbody = $('<tbody />')
-                               .appendTo(table);
+               _render_table_row: function(sid, index)
+               {
+                       var row = $('<tr />')
+                               .attr('data-luci2-sid', sid);
 
-                       if (s.length == 0)
+                       for (var j = 0; j < this.tabs[0].fields.length; j++)
                        {
-                               $('<tr />')
-                                       .addClass('cbi-section-table-row')
-                                       .append(
-                                               $('<td />')
-                                                       .addClass('cbi-section-table-cell')
-                                                       .addClass('cbi-section-table-placeholder')
-                                                       .attr('colspan', thead.children().children().length)
-                                                       .text(this.options.placeholder || _luci2.tr('This section contains no values yet')))
-                                       .appendTo(tbody);
+                               row.append($('<td />')
+                                       .css('width', this.tabs[0].fields[j].options.width || '')
+                                       .append(this.tabs[0].fields[j].render(sid, true)));
                        }
 
-                       for (var i = 0; i < s.length; i++)
+                       if (this.options.addremove !== false || this.options.sortable)
                        {
-                               var sid = s[i]['.name'];
-                               var inst = this.instance[sid] = { tabs: [ ] };
+                               row.append($('<td />')
+                                       .addClass('text-right')
+                                       .append($('<div />')
+                                               .addClass('btn-group')
+                                               .append(this._render_sort(sid, index))
+                                               .append(this._render_remove(sid, index))));
+                       }
 
-                               var row = $('<tr />')
-                                       .addClass('cbi-section-table-row')
-                                       .appendTo(tbody);
+                       return row;
+               },
 
-                               for (var j = 0; j < this.tabs[0].fields.length; j++)
-                               {
-                                       $('<td />')
-                                               .addClass('cbi-section-table-cell')
-                                               .css('width', this.tabs[0].fields[j].options.width || '')
-                                               .append(this.tabs[0].fields[j].render(sid, true))
-                                               .appendTo(row);
-                               }
+               _render_table_body: function()
+               {
+                       var s = this.sections();
 
-                               if (this.options.sortable)
-                               {
-                                       $('<td />')
-                                               .addClass('cbi-section-table-cell')
-                                               .addClass('cbi-section-table-sort')
-                                               .append($('<img />').attr('src', _luci2.globals.resource + '/icons/cbi/up.gif').attr('title', _luci2.tr('Drag to sort')))
-                                               .append($('<br />'))
-                                               .append($('<img />').attr('src', _luci2.globals.resource + '/icons/cbi/down.gif').attr('title', _luci2.tr('Drag to sort')))
-                                               .appendTo(row);
-                               }
+                       var tbody = $('<tbody />');
 
-                               if (this.options.addremove !== false)
-                               {
-                                       $('<td />')
-                                               .addClass('cbi-section-table-cell')
-                                               .append(this._render_remove(sid))
-                                               .appendTo(row);
-                               }
+                       if (s.length == 0)
+                       {
+                               var cols = this.tabs[0].fields.length;
 
-                               this.validate(sid);
+                               if (this.options.addremove !== false || this.options.sortable)
+                                       cols++;
 
-                               row = null;
+                               tbody.append($('<tr />')
+                                       .append($('<td />')
+                                               .addClass('text-muted')
+                                               .attr('colspan', cols)
+                                               .text(this.label('placeholder') || _luci2.tr('There are no entries defined yet.'))));
                        }
 
-                       if (this.options.sortable)
+                       for (var i = 0; i < s.length; i++)
                        {
-                               var s = tbody.sortable({
-                                       handle: 'td.cbi-section-table-sort'
-                               });
-
-                               s.on('sortupdate', { self: this, ids: s.sortable('toArray') }, function(ev, ui) {
-                                       var sections = [ ];
-                                       for (var i = 0; i < ev.data.ids.length; i++)
-                                               sections.push(ev.data.ids[i].substring(ev.data.ids[i].lastIndexOf('.') + 1));
-                                       _luci2.uci.order(ev.data.self.map.uci_package, sections);
-                               });
+                               var sid = s[i]['.name'];
+                               var inst = this.instance[sid] = { tabs: [ ] };
 
-                               s.on('sortstop', function(ev, ui) {
-                                       ui.item.children('div.cbi-section-head').triggerHandler('focusout');
-                               });
+                               tbody.append(this._render_table_row(sid, i));
                        }
 
-                       if (this.options.addremove)
-                               this._render_add().appendTo(fieldset);
-
-                       fields = table = thead = tbody = null;
+                       return tbody;
+               },
 
-                       return fieldset;
+               _render_body: function(condensed)
+               {
+                       return $('<table />')
+                               .addClass('table table-condensed table-hover')
+                               .append(this._render_table_head())
+                               .append(this._render_table_body());
                }
        });
 
@@ -4760,7 +5944,7 @@ function LuCI2()
                }
        });
 
-       this.cbi.Map = AbstractWidget.extend({
+       this.cbi.Map = this.ui.AbstractWidget.extend({
                init: function(uci_package, options)
                {
                        var self = this;
@@ -4777,18 +5961,49 @@ function LuCI2()
                        });
                },
 
+               _load_cb: function(packages)
+               {
+                       for (var i = 0; i < packages.length; i++)
+                       {
+                               this.uci.values[packages[i]['.package']] = packages[i];
+                               delete packages[i]['.package'];
+                       }
+
+                       var deferreds = [ _luci2.deferrable(this.options.prepare()) ];
+
+                       for (var i = 0; i < this.sections.length; i++)
+                       {
+                               for (var f in this.sections[i].fields)
+                               {
+                                       if (typeof(this.sections[i].fields[f].load) != 'function')
+                                               continue;
+
+                                       var s = this.sections[i].sections();
+                                       for (var j = 0; j < s.length; j++)
+                                       {
+                                               var rv = this.sections[i].fields[f].load(s[j]['.name']);
+                                               if (_luci2.isDeferred(rv))
+                                                       deferreds.push(rv);
+                                       }
+                               }
+                       }
+
+                       return $.when.apply($, deferreds);
+               },
+
                load: function()
                {
+                       var self = this;
+
                        this.uci = {
                                newid:   0,
                                values:  { },
                                creates: { },
                                changes: { },
-                               deletes: { }
+                               deletes: { },
+                               reorder: false
                        };
 
-                       this.active_panel = 0;
-
                        var packages = { };
 
                        for (var i = 0; i < this.sections.length; i++)
@@ -4796,126 +6011,125 @@ function LuCI2()
 
                        packages[this.uci_package] = true;
 
-                       for (var p in packages)
-                               packages[p] = ['uci', 'get', { config: p }];
+                       _luci2.rpc.batch();
 
-                       var load_cb = this._load_cb || (this._load_cb = $.proxy(function(responses) {
-                               for (var p in responses)
-                               {
-                                       if (responses[p][0] != 0 || !responses[p][1] || !responses[p][1].values)
-                                               continue;
+                       for (var pkg in packages)
+                               _luci2.uci.get_all(pkg);
 
-                                       this.uci.values[p] = responses[p][1].values;
-                               }
+                       return _luci2.rpc.flush().then(function(packages) {
+                               return self._load_cb(packages);
+                       });
+               },
 
-                               var deferreds = [ _luci2.deferrable(this.options.prepare()) ];
+               _ev_tab: function(ev)
+               {
+                       var self = ev.data.self;
 
-                               for (var i = 0; i < this.sections.length; i++)
-                               {
-                                       for (var f in this.sections[i].fields)
-                                       {
-                                               if (typeof(this.sections[i].fields[f].load) != 'function')
-                                                       continue;
+                       self.validate();
+                       self.active_tab = parseInt(ev.target.getAttribute('data-luci2-tab-index'));
+               },
 
-                                               var s = this.sections[i].sections();
-                                               for (var j = 0; j < s.length; j++)
-                                               {
-                                                       var rv = this.sections[i].fields[f].load(s[j]['.name']);
-                                                       if (_luci2.deferred(rv))
-                                                               deferreds.push(rv);
-                                               }
-                                       }
-                               }
+               _render_tab_head: function(tab_index)
+               {
+                       var section = this.sections[tab_index];
+                       var cur = this.active_tab || 0;
+
+                       var tabh = $('<li />')
+                               .append($('<a />')
+                                       .attr('id', section.id('sectiontab'))
+                                       .attr('href', '#' + section.id('section'))
+                                       .attr('data-toggle', 'tab')
+                                       .attr('data-luci2-tab-index', tab_index)
+                                       .text(section.label('caption') + ' ')
+                                       .append($('<span />')
+                                               .addClass('badge'))
+                                       .on('shown.bs.tab', { self: this }, this._ev_tab));
 
-                               return $.when.apply($, deferreds);
-                       }, this));
+                       if (cur == tab_index)
+                               tabh.addClass('active');
 
-                       return _luci2.rpc.call(packages).then(load_cb);
+                       return tabh;
                },
 
-               render: function()
+               _render_tab_body: function(tab_index)
                {
-                       var map = $('<div />').addClass('cbi-map');
-
-                       if (typeof(this.options.caption) == 'string')
-                               $('<h2 />').text(this.options.caption).appendTo(map);
-
-                       if (typeof(this.options.description) == 'string')
-                               $('<div />').addClass('cbi-map-descr').text(this.options.description).appendTo(map);
+                       var section = this.sections[tab_index];
+                       var desc = section.label('description');
+                       var cur = this.active_tab || 0;
 
-                       var sections = $('<div />').appendTo(map);
+                       var tabb = $('<div />')
+                               .addClass('tab-pane')
+                               .attr('id', section.id('section'))
+                               .attr('data-luci2-tab-index', tab_index);
 
-                       for (var i = 0; i < this.sections.length; i++)
-                       {
-                               var s = this.sections[i].render();
+                       if (cur == tab_index)
+                               tabb.addClass('active');
 
-                               if (this.options.readonly || this.sections[i].options.readonly)
-                                       s.find('input, select, button, img.cbi-button').attr('disabled', true);
+                       if (desc)
+                               tabb.append($('<p />')
+                                       .text(desc));
 
-                               s.appendTo(sections);
+                       var s = section.render(this.options.tabbed);
 
-                               if (this.sections[i].options.active)
-                                       this.active_panel = i;
-                       }
+                       if (this.options.readonly || section.options.readonly)
+                               s.find('input, select, button, img.cbi-button').attr('disabled', true);
 
-                       if (this.options.collabsible)
-                       {
-                               var a = sections.accordion({
-                                       header: '> fieldset > div.cbi-section-head',
-                                       heightStyle: 'content',
-                                       active: this.active_panel
-                               });
+                       tabb.append(s);
 
-                               a.on('accordionbeforeactivate', { self: this }, function(ev, ui) {
-                                       var h = ui.oldHeader;
-                                       var s = ev.data.self.sections[parseInt(h.attr('cbi-section-num'))];
-                                       var i = h.attr('cbi-section-id');
+                       return tabb;
+               },
 
-                                       h.children('.cbi-section-teaser').empty().append(s.teaser(i));
+               _render_body: function()
+               {
+                       var tabs = $('<ul />')
+                               .addClass('nav nav-tabs');
 
-                                       for (var i = 0; i < ev.data.self.sections.length; i++)
-                                               ev.data.self.sections[i].validate();
-                               });
+                       var body = $('<div />')
+                               .append(tabs);
 
-                               a.on('accordionactivate', { self: this }, function(ev, ui) {
-                                       ev.data.self.active_panel = parseInt(ui.newPanel.attr('index'));
-                               });
+                       for (var i = 0; i < this.sections.length; i++)
+                       {
+                               tabs.append(this._render_tab_head(i));
+                               body.append(this._render_tab_body(i));
                        }
 
-                       if (this.options.pageaction !== false)
-                       {
-                               var a = $('<div />')
-                                       .addClass('cbi-page-actions')
-                                       .appendTo(map);
+                       if (this.options.tabbed)
+                               body.addClass('tab-content');
+                       else
+                               tabs.hide();
 
-                               $('<input />')
-                                       .addClass('cbi-button').addClass('cbi-button-apply')
-                                       .attr('type', 'button')
-                                       .val(_luci2.tr('Save & Apply'))
-                                       .appendTo(a);
+                       return body;
+               },
 
-                               $('<input />')
-                                       .addClass('cbi-button').addClass('cbi-button-save')
-                                       .attr('type', 'button')
-                                       .val(_luci2.tr('Save'))
-                                       .click({ self: this }, function(ev) { ev.data.self.send(); })
-                                       .appendTo(a);
+               render: function()
+               {
+                       var map = $('<form />');
 
-                               $('<input />')
-                                       .addClass('cbi-button').addClass('cbi-button-reset')
-                                       .attr('type', 'button')
-                                       .val(_luci2.tr('Reset'))
-                                       .click({ self: this }, function(ev) { ev.data.self.insertInto(ev.data.self.target); })
-                                       .appendTo(a);
+                       if (typeof(this.options.caption) == 'string')
+                               map.append($('<h2 />')
+                                       .text(this.options.caption));
 
-                               a = null;
-                       }
+                       if (typeof(this.options.description) == 'string')
+                               map.append($('<p />')
+                                       .text(this.options.description));
 
-                       var top = $('<form />').append(map);
+                       map.append(this._render_body());
 
-                       map = null;
+                       if (this.options.pageaction !== false)
+                       {
+                               map.append($('<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); }))));
+                       }
 
-                       return top;
+                       return map;
                },
 
                finish: function()
@@ -4937,7 +6151,7 @@ function LuCI2()
                {
                        var w = widget ? new widget(uci_type, options) : null;
 
-                       if (!(w instanceof AbstractSection))
+                       if (!(w instanceof _luci2.cbi.AbstractSection))
                                throw 'Widget must be an instance of AbstractSection';
 
                        w.map = this;
@@ -4976,7 +6190,8 @@ function LuCI2()
                                '.type':      type,
                                '.name':      s,
                                '.create':    name,
-                               '.anonymous': !name
+                               '.anonymous': !name,
+                               '.index':     1000 + this.uci.newid
                        };
 
                        return s;
@@ -5020,12 +6235,17 @@ function LuCI2()
                                if (!del || del[s] !== true)
                                        sa.push(pkg[s]);
 
-                       sa.sort(function(a, b) { return a['.index'] - b['.index'] });
-
                        if (crt)
                                for (var s in crt)
                                        sa.push(crt[s]);
 
+                       sa.sort(function(a, b) {
+                               return a['.index'] - b['.index'];
+                       });
+
+                       for (var i = 0; i < sa.length; i++)
+                               sa[i]['.index'] = i;
+
                        if (typeof(cb) == 'function')
                                for (var i = 0; i < sa.length; i++)
                                        cb.apply(this, [ sa[i] ]);
@@ -5127,8 +6347,10 @@ function LuCI2()
                        var rv = true;
 
                        for (var i = 0; i < this.sections.length; i++)
+                       {
                                if (!this.sections[i].validate())
                                        rv = false;
+                       }
 
                        return rv;
                },
@@ -5154,7 +6376,7 @@ function LuCI2()
                                        for (var j = 0; j < s.length; j++)
                                        {
                                                var rv = this.sections[i].fields[f].save(s[j]['.name']);
-                                               if (_luci2.deferred(rv))
+                                               if (_luci2.isDeferred(rv))
                                                        deferreds.push(rv);
                                        }
                                }
@@ -5163,97 +6385,124 @@ function LuCI2()
                        return $.when.apply($, deferreds);
                },
 
-               send: function()
+               _send_uci_reorder: function()
                {
-                       if (!this.validate())
+                       if (!this.uci.reorder)
                                return _luci2.deferrable();
 
-                       var send_cb = this._send_cb || (this._send_cb = $.proxy(function() {
-                               var requests = [ ];
+                       _luci2.rpc.batch();
 
-                               if (this.uci.creates)
-                                       for (var c in this.uci.creates)
-                                               for (var s in this.uci.creates[c])
-                                               {
-                                                       var r = {
-                                                               config: c,
-                                                               values: { }
-                                                       };
+                       /*
+                        gather all created and existing sections, sort them according
+                        to their index value and issue an uci order call
+                       */
+                       for (var c in this.uci.values)
+                       {
+                               var o = [ ];
 
-                                                       for (var k in this.uci.creates[c][s])
-                                                       {
-                                                               if (k == '.type')
-                                                                       r.type = this.uci.creates[i][k];
-                                                               else if (k == '.create')
-                                                                       r.name = this.uci.creates[i][k];
-                                                               else if (k.charAt(0) != '.')
-                                                                       r.values[k] = this.uci.creates[i][k];
-                                                       }
-                                                       requests.push(['uci', 'add', r]);
-                                               }
+                               if (this.uci.creates && this.uci.creates[c])
+                                       for (var s in this.uci.creates[c])
+                                               o.push(this.uci.creates[c][s]);
+
+                               for (var s in this.uci.values[c])
+                                       o.push(this.uci.values[c][s]);
+
+                               if (o.length > 0)
+                               {
+                                       o.sort(function(a, b) {
+                                               return (a['.index'] - b['.index']);
+                                       });
+
+                                       var sids = [ ];
+
+                                       for (var i = 0; i < o.length; i++)
+                                               sids.push(o[i]['.name']);
+
+                                       _luci2.uci.order(c, sids);
+                               }
+                       }
+
+                       return _luci2.rpc.flush();
+               },
+
+               _send_uci: function()
+               {
+                       _luci2.rpc.batch();
+
+                       var self = this;
+                       var snew = [ ];
+
+                       if (this.uci.creates)
+                               for (var c in this.uci.creates)
+                                       for (var s in this.uci.creates[c])
+                                       {
+                                               var r = {
+                                                       config: c,
+                                                       values: { }
+                                               };
 
-                               if (this.uci.changes)
-                                       for (var c in this.uci.changes)
-                                               for (var s in this.uci.changes[c])
-                                                       requests.push(['uci', 'set', {
-                                                               config:  c,
-                                                               section: s,
-                                                               values:  this.uci.changes[c][s]
-                                                       }]);
-
-                               if (this.uci.deletes)
-                                       for (var c in this.uci.deletes)
-                                               for (var s in this.uci.deletes[c])
+                                               for (var k in this.uci.creates[c][s])
                                                {
-                                                       var o = this.uci.deletes[c][s];
-                                                       requests.push(['uci', 'delete', {
-                                                               config:  c,
-                                                               section: s,
-                                                               options: (o === true) ? undefined : o
-                                                       }]);
+                                                       if (k == '.type')
+                                                               r.type = this.uci.creates[c][s][k];
+                                                       else if (k == '.create')
+                                                               r.name = this.uci.creates[c][s][k];
+                                                       else if (k.charAt(0) != '.')
+                                                               r.values[k] = this.uci.creates[c][s][k];
                                                }
 
-                               return _luci2.rpc.call(requests);
-                       }, this));
+                                               snew.push(this.uci.creates[c][s]);
 
-                       var self = this;
+                                               _luci2.uci.add(r.config, r.type, r.name, r.values);
+                                       }
 
-                       _luci2.ui.loading(true);
+                       if (this.uci.changes)
+                               for (var c in this.uci.changes)
+                                       for (var s in this.uci.changes[c])
+                                               _luci2.uci.set(c, s, this.uci.changes[c][s]);
 
-                       return this.save().then(send_cb).then(function() {
-                               return self.load();
-                       }).then(function() {
-                               self.redraw();
-                               self = null;
+                       if (this.uci.deletes)
+                               for (var c in this.uci.deletes)
+                                       for (var s in this.uci.deletes[c])
+                                       {
+                                               var o = this.uci.deletes[c][s];
+                                               _luci2.uci['delete'](c, s, (o === true) ? undefined : o);
+                                       }
 
-                               _luci2.ui.loading(false);
+                       return _luci2.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
+                               */
+                               for (var i = 0; i < snew.length; i++)
+                                       snew[i]['.name'] = responses[i];
+
+                               return self._send_uci_reorder();
                        });
                },
 
-               dialog: function(id)
+               send: function()
                {
-                       var d = $('<div />');
-                       var p = $('<p />');
+                       if (!this.validate())
+                               return _luci2.deferrable();
 
-                       $('<img />')
-                               .attr('src', _luci2.globals.resource + '/icons/loading.gif')
-                               .css('vertical-align', 'middle')
-                               .css('padding-right', '10px')
-                               .appendTo(p);
+                       var self = this;
 
-                       p.append(_luci2.tr('Loading data...'));
+                       _luci2.ui.saveScrollTop();
+                       _luci2.ui.loading(true);
 
-                       p.appendTo(d);
-                       d.appendTo(id);
+                       return this.save().then(function() {
+                               return self._send_uci();
+                       }).then(function() {
+                               return _luci2.ui.updateChanges();
+                       }).then(function() {
+                               return self.load();
+                       }).then(function() {
+                               self.redraw();
+                               self = null;
 
-                       return d.dialog({
-                               modal: true,
-                               draggable: false,
-                               resizable: false,
-                               height: 90,
-                               open: function() {
-                                       $(this).parent().children('.ui-dialog-titlebar').hide();
-                               }
+                               _luci2.ui.loading(false);
+                               _luci2.ui.restoreScrollTop();
                        });
                },