luci2: expose uci reorder deltas in uci changelog view
[project/luci2/ui.git] / luci2 / htdocs / luci2 / luci2.js
index 98cd441..13519e6 100644 (file)
@@ -214,7 +214,7 @@ function LuCI2()
                _class.prototype = prototype;
                _class.prototype.constructor = _class;
 
-               _class.extend = arguments.callee;
+               _class.extend = Class.extend;
 
                return _class;
        };
@@ -364,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)
@@ -386,6 +388,103 @@ function LuCI2()
                return data;
        };
 
+       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.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;
+                       }
+               }
+
+               return { };
+       };
+
+       this.filterArray = function(array, item)
+       {
+               if (!$.isArray(array))
+                       return [ ];
+
+               for (var i = 0; i < array.length; i++)
+                       if (array[i] === item)
+                               array.splice(i--, 1);
+
+               return array;
+       };
+
+       this.toClassName = function(str, suffix)
+       {
+               var n = '';
+               var l = str.split(/[\/.]/);
+
+               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 (typeof(suffix) == 'string')
+                       n += suffix;
+
+               return n;
+       };
+
        this.globals = {
                timeout:  15000,
                resource: '/luci2',
@@ -406,43 +505,48 @@ function LuCI2()
                                data:        JSON.stringify(req),
                                dataType:    'json',
                                type:        'POST',
-                               timeout:     _luci2.globals.timeout
-                       }).then(cb);
+                               timeout:     _luci2.globals.timeout,
+                               _rpc_req:   req
+                       }).then(cb, cb);
                },
 
                _list_cb: function(msg)
                {
+                       var list = msg.result;
+
                        /* verify message frame */
-                       if (typeof(msg) != 'object' || msg.jsonrpc != '2.0' || !msg.id)
-                               throw 'Invalid JSON response';
+                       if (typeof(msg) != 'object' || msg.jsonrpc != '2.0' || !msg.id || !$.isArray(list))
+                               list = [ ];
 
-                       return msg.result;
+                       return $.Deferred().resolveWith(this, [ list ]);
                },
 
                _call_cb: function(msg)
                {
                        var data = [ ];
                        var type = Object.prototype.toString;
+                       var reqs = this._rpc_req;
 
-                       if (!$.isArray(msg))
+                       if (!$.isArray(reqs))
+                       {
                                msg = [ msg ];
+                               reqs = [ reqs ];
+                       }
 
                        for (var i = 0; i < msg.length; i++)
                        {
-                               /* verify message frame */
-                               if (typeof(msg[i]) != 'object' || msg[i].jsonrpc != '2.0' || !msg[i].id)
-                                       throw 'Invalid JSON response';
-
                                /* fetch related request info */
-                               var req = _luci2.rpc._requests[msg[i].id];
+                               var req = _luci2.rpc._requests[reqs[i].id];
                                if (typeof(req) != 'object')
                                        throw 'No related request for JSON response';
 
                                /* fetch response attribute and verify returned type */
                                var ret = undefined;
 
-                               if ($.isArray(msg[i].result) && msg[i].result[0] == 0)
-                                       ret = (msg[i].result.length > 1) ? msg[i].result[1] : msg[i].result[0];
+                               /* 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];
 
                                if (req.expect)
                                {
@@ -451,7 +555,7 @@ function LuCI2()
                                                if (typeof(ret) != 'undefined' && key != '')
                                                        ret = ret[key];
 
-                                               if (type.call(ret) != type.call(req.expect[key]))
+                                               if (typeof(ret) == 'undefined' || type.call(ret) != type.call(req.expect[key]))
                                                        ret = req.expect[key];
 
                                                break;
@@ -473,10 +577,10 @@ function LuCI2()
                                        data = ret;
 
                                /* delete request object */
-                               delete _luci2.rpc._requests[msg[i].id];
+                               delete _luci2.rpc._requests[reqs[i].id];
                        }
 
-                       return data;
+                       return $.Deferred().resolveWith(this, [ data ]);
                },
 
                list: function()
@@ -565,679 +669,2103 @@ function LuCI2()
                }
        };
 
-       this.uci = {
+       this.UCIContext = Class.extend({
 
-               writable: function()
+               init: function()
                {
-                       return _luci2.session.access('ubus', 'uci', 'commit');
+                       this.state = {
+                               newid:   0,
+                               values:  { },
+                               creates: { },
+                               changes: { },
+                               deletes: { },
+                               reorder: { }
+                       };
                },
 
-               add: _luci2.rpc.declare({
+               _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: '' }
                }),
 
-               apply: function()
-               {
-
-               },
-
-               configs: _luci2.rpc.declare({
+               _set: _luci2.rpc.declare({
                        object: 'uci',
-                       method: 'configs',
-                       expect: { configs: [ ] }
+                       method: 'set',
+                       params: [ 'config', 'section', 'values' ]
                }),
 
-               _changes: _luci2.rpc.declare({
+               _delete: _luci2.rpc.declare({
                        object: 'uci',
-                       method: 'changes',
-                       params: [ 'config' ],
-                       expect: { changes: [ ] }
+                       method: 'delete',
+                       params: [ 'config', 'section', 'options' ]
                }),
 
-               changes: function(config)
+               load: function(packages)
                {
-                       if (typeof(config) == 'string')
-                               return this._changes(config);
+                       var self = this;
+                       var seen = { };
+                       var pkgs = [ ];
 
-                       var configlist;
-                       return this.configs().then(function(configs) {
-                               _luci2.rpc.batch();
-                               configlist = configs;
+                       if (!$.isArray(packages))
+                               packages = [ packages ];
 
-                               for (var i = 0; i < configs.length; i++)
-                                       _luci2.uci._changes(configs[i]);
+                       _luci2.rpc.batch();
 
-                               return _luci2.rpc.flush();
-                       }).then(function(changes) {
-                               var rv = { };
+                       for (var i = 0; i < packages.length; i++)
+                               if (!seen[packages[i]])
+                               {
+                                       pkgs.push(packages[i]);
+                                       seen[packages[i]] = true;
+                                       self._load(packages[i]);
+                               }
 
-                               for (var i = 0; i < configlist.length; i++)
-                                       if (changes[i].length)
-                                               rv[configlist[i]] = changes[i];
+                       return _luci2.rpc.flush().then(function(responses) {
+                               for (var i = 0; i < responses.length; i++)
+                                       self.state.values[pkgs[i]] = responses[i];
 
-                               return rv;
+                               return pkgs;
                        });
                },
 
-               commit: _luci2.rpc.declare({
-                       object: 'uci',
-                       method: 'commit',
-                       params: [ 'config' ]
-               }),
-
-               _delete_one: _luci2.rpc.declare({
-                       object: 'uci',
-                       method: 'delete',
-                       params: [ 'config', 'section', 'option' ]
-               }),
+               unload: function(packages)
+               {
+                       if (!$.isArray(packages))
+                               packages = [ packages ];
 
-               _delete_multiple: _luci2.rpc.declare({
-                       object: 'uci',
-                       method: 'delete',
-                       params: [ 'config', 'section', 'options' ]
-               }),
+                       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]];
+                       }
+               },
 
-               'delete': function(config, section, option)
+               add: function(conf, type, name)
                {
-                       if ($.isArray(option))
-                               return this._delete_multiple(config, section, option);
-                       else
-                               return this._delete_one(config, section, option);
-               },
+                       var c = this.state.creates;
+                       var s = '.new.%d'.format(this.state.newid++);
 
-               delete_all: _luci2.rpc.declare({
-                       object: 'uci',
-                       method: 'delete',
-                       params: [ 'config', 'type', 'match' ]
-               }),
+                       if (!c[conf])
+                               c[conf] = { };
 
-               _foreach: _luci2.rpc.declare({
-                       object: 'uci',
-                       method: 'get',
-                       params: [ 'config', 'type' ],
-                       expect: { values: { } }
-               }),
+                       c[conf][s] = {
+                               '.type':      type,
+                               '.name':      s,
+                               '.create':    name,
+                               '.anonymous': !name,
+                               '.index':     1000 + this.state.newid
+                       };
 
-               foreach: function(config, type, cb)
-               {
-                       return this._foreach(config, type).then(function(sections) {
-                               for (var s in sections)
-                                       cb(sections[s]);
-                       });
+                       return s;
                },
 
-               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;
-                       }
-               }),
+               remove: function(conf, sid)
+               {
+                       var n = this.state.creates;
+                       var c = this.state.changes;
+                       var d = this.state.deletes;
 
-               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;
+                       /* requested deletion of a just created section */
+                       if (sid.indexOf('.new.') == 0)
+                       {
+                               if (n[conf])
+                                       delete n[conf][sid];
                        }
-               }),
-
-               get_first: function(config, type, option)
-               {
-                       return this._foreach(config, type).then(function(sections) {
-                               for (var s in sections)
-                               {
-                                       var val = (typeof(option) == 'string') ? sections[s][option] : sections[s]['.name'];
+                       else
+                       {
+                               if (c[conf])
+                                       delete c[conf][sid];
 
-                                       if (typeof(val) != 'undefined')
-                                               return val;
-                               }
+                               if (!d[conf])
+                                       d[conf] = { };
 
-                               return undefined;
-                       });
+                               d[conf][sid] = true;
+                       }
                },
 
-               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)
+               sections: function(conf, type, 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 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];
 
-                       var values = { };
-                           values[option] = value;
+                       if (!v)
+                               return sa;
 
-                       return this._set(config, section, values);
-               },
+                       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));
 
-               order: _luci2.rpc.declare({
-                       object: 'uci',
-                       method: 'order',
-                       params: [ 'config', 'sections' ]
-               })
-       };
+                       if (n)
+                               for (var s in n)
+                                       if (!type || n[s]['.type'] == type)
+                                               sa.push(n[s]);
 
-       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;
+                       sa.sort(function(a, b) {
+                               return a['.index'] - b['.index'];
                        });
-               },
 
-               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;
-                       }
-               }),
+                       for (var i = 0; i < sa.length; i++)
+                               sa[i]['.index'] = i;
 
-               getNetworkStatus: function()
-               {
-                       var nets = [ ];
-                       var devs = { };
+                       if (typeof(cb) == 'function')
+                               for (var i = 0; i < sa.length; i++)
+                                       cb.call(this, sa[i], sa[i]['.name']);
 
-                       return this.listNetworkNames().then(function(names) {
-                               _luci2.rpc.batch();
+                       return sa;
+               },
 
-                               for (var i = 0; i < names.length; i++)
-                                       _luci2.network.getInterfaceStatus(names[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;
 
-                               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] = { });
-                               }
+                       if (typeof(sid) == 'undefined')
+                               return undefined;
 
-                               _luci2.rpc.batch();
+                       /* requested option in a just created section */
+                       if (sid.indexOf('.new.') == 0)
+                       {
+                               if (!n[conf])
+                                       return undefined;
 
-                               for (var dev in devs)
-                                       _luci2.network.getDeviceStatus(dev);
+                               if (typeof(opt) == 'undefined')
+                                       return n[conf][sid];
 
-                               return _luci2.rpc.flush();
-                       }).then(function(devices) {
-                               _luci2.rpc.batch();
+                               return n[conf][sid][opt];
+                       }
 
-                               for (var i = 0; i < devices.length; i++)
+                       /* requested an option value */
+                       if (typeof(opt) != 'undefined')
+                       {
+                               /* check whether option was deleted */
+                               if (d[conf] && d[conf][sid])
                                {
-                                       var brm = devices[i]['bridge-members'];
-                                       delete devices[i]['bridge-members'];
-
-                                       $.extend(devs[devices[i]['device']], devices[i]);
-
-                                       if (!brm)
-                                               continue;
-
-                                       devs[devices[i]['device']].subdevices = [ ];
-
-                                       for (var j = 0; j < brm.length; j++)
-                                       {
-                                               if (!devs[brm[j]])
-                                               {
-                                                       devs[brm[j]] = { };
-                                                       _luci2.network.getDeviceStatus(brm[j]);
-                                               }
+                                       if (d[conf][sid] === true)
+                                               return undefined;
 
-                                               devs[devices[i]['device']].subdevices[j] = devs[brm[j]];
-                                       }
+                                       for (var i = 0; i < d[conf][sid].length; i++)
+                                               if (d[conf][sid][i] == opt)
+                                                       return undefined;
                                }
 
-                               return _luci2.rpc.flush();
-                       }).then(function(subdevices) {
-                               for (var i = 0; i < subdevices.length; i++)
-                                       $.extend(devs[subdevices[i]['device']], subdevices[i]);
-
-                               _luci2.rpc.batch();
+                               /* check whether option was changed */
+                               if (c[conf] && c[conf][sid] && typeof(c[conf][sid][opt]) != 'undefined')
+                                       return c[conf][sid][opt];
 
-                               for (var dev in devs)
-                                       _luci2.wireless.getDeviceStatus(dev);
+                               /* return base value */
+                               if (v[conf] && v[conf][sid])
+                                       return v[conf][sid][opt];
 
-                               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 undefined;
+                       }
 
-                               nets.sort(function(a, b) {
-                                       if (a['interface'] < b['interface'])
-                                               return -1;
-                                       else if (a['interface'] > b['interface'])
-                                               return 1;
-                                       else
-                                               return 0;
-                               });
+                       /* requested an entire section */
+                       if (v[conf])
+                               return v[conf][sid];
 
-                               return nets;
-                       });
+                       return undefined;
                },
 
-               findWanInterfaces: function(cb)
+               set: function(conf, sid, opt, val)
                {
-                       return this.listNetworkNames().then(function(names) {
-                               _luci2.rpc.batch();
+                       var n = this.state.creates;
+                       var c = this.state.changes;
+                       var d = this.state.deletes;
 
-                               for (var i = 0; i < names.length; i++)
-                                       _luci2.network.getInterfaceStatus(names[i]);
-
-                               return _luci2.rpc.flush();
-                       }).then(function(interfaces) {
-                               var rv = [ undefined, undefined ];
+                       if (typeof(sid) == 'undefined' ||
+                           typeof(opt) == 'undefined' ||
+                           opt.charAt(0) == '.')
+                               return;
 
-                               for (var i = 0; i < interfaces.length; i++)
+                       if (sid.indexOf('.new.') == 0)
+                       {
+                               if (n[conf] && n[conf][sid])
                                {
-                                       if (!interfaces[i].route)
-                                               continue;
-
-                                       for (var j = 0; j < interfaces[i].route.length; j++)
-                                       {
-                                               var rt = interfaces[i].route[j];
-
-                                               if (typeof(rt.table) != 'undefined')
-                                                       continue;
+                                       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 (rt.target == '0.0.0.0' && rt.mask == 0)
-                                                       rv[0] = interfaces[i];
-                                               else if (rt.target == '::' && rt.mask == 0)
-                                                       rv[1] = interfaces[i];
-                                       }
+                               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);
                                }
+                       }
 
-                               return rv;
+                       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])
+                                       {
+                                               var r = {
+                                                       config: c,
+                                                       values: { }
+                                               };
+
+                                               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();
                        });
                },
 
-               getDHCPLeases: _luci2.rpc.declare({
-                       object: 'luci2.network',
-                       method: 'dhcp_leases',
-                       expect: { leases: [ ] }
+               _apply: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'apply',
+                       params: [ 'timeout', 'rollback' ]
                }),
 
-               getDHCPv6Leases: _luci2.rpc.declare({
-                       object: 'luci2.network',
-                       method: 'dhcp6_leases',
-                       expect: { leases: [ ] }
+               _confirm: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'confirm'
                }),
 
-               getRoutes: _luci2.rpc.declare({
-                       object: 'luci2.network',
-                       method: 'routes',
-                       expect: { routes: [ ] }
-               }),
+               apply: function(timeout)
+               {
+                       var self = this;
+                       var date = new Date();
+                       var deferred = $.Deferred();
 
-               getIPv6Routes: _luci2.rpc.declare({
-                       object: 'luci2.network',
-                       method: 'routes',
-                       expect: { routes: [ ] }
-               }),
+                       if (typeof(timeout) != 'number' || timeout < 1)
+                               timeout = 10;
 
-               getARPTable: _luci2.rpc.declare({
-                       object: 'luci2.network',
-                       method: 'arp_table',
-                       expect: { entries: [ ] }
+                       self._apply(timeout, true).then(function(rv) {
+                               if (rv != 0)
+                               {
+                                       deferred.rejectWith(self, [ rv ]);
+                                       return;
+                               }
+
+                               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;
+               },
+
+               changes: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'changes',
+                       expect: { changes: { } }
                }),
 
-               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'];
+               readable: function(conf)
+               {
+                       return _luci2.session.hasACL('uci', conf, 'read');
+               },
+
+               writable: function(conf)
+               {
+                       return _luci2.session.hasACL('uci', conf, 'write');
+               }
+       });
+
+       this.uci = new this.UCIContext();
+
+       this.wireless = {
+               listDeviceNames: _luci2.rpc.declare({
+                       object: 'iwinfo',
+                       method: 'devices',
+                       expect: { 'devices': [ ] },
+                       filter: function(data) {
+                               data.sort();
                                return data;
                        }
                }),
 
                getDeviceStatus: _luci2.rpc.declare({
-                       object: 'network.device',
-                       method: 'status',
-                       params: [ 'name' ],
+                       object: 'iwinfo',
+                       method: 'info',
+                       params: [ 'device' ],
                        expect: { '': { } },
                        filter: function(data, params) {
-                               data['device'] = params['name'];
-                               return data;
+                               if (!$.isEmptyObject(data))
+                               {
+                                       data['device'] = params['device'];
+                                       return data;
+                               }
+                               return undefined;
                        }
                }),
 
-               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: { } },
+               getAssocList: _luci2.rpc.declare({
+                       object: 'iwinfo',
+                       method: 'assoclist',
+                       params: [ 'device' ],
+                       expect: { results: [ ] },
                        filter: function(data, params) {
-                               data['attrs']      = data['switch'];
-                               data['vlan_attrs'] = data['vlan'];
-                               data['port_attrs'] = data['port'];
-                               data['switch']     = params['switch'];
+                               for (var i = 0; i < data.length; i++)
+                                       data[i]['device'] = params['device'];
 
-                               delete data.vlan;
-                               delete data.port;
+                               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;
                        }
                }),
 
-               getSwitchStatus: _luci2.rpc.declare({
-                       object: 'luci2.network',
-                       method: 'switch_status',
-                       params: [ 'switch' ],
-                       expect: { ports: [ ] }
-               }),
+               getWirelessStatus: function() {
+                       return this.listDeviceNames().then(function(names) {
+                               _luci2.rpc.batch();
 
+                               for (var i = 0; i < names.length; i++)
+                                       _luci2.wireless.getDeviceStatus(names[i]);
 
-               runPing: _luci2.rpc.declare({
-                       object: 'luci2.network',
-                       method: 'ping',
-                       params: [ 'data' ],
-                       expect: { '': { code: -1 } }
-               }),
+                               return _luci2.rpc.flush();
+                       }).then(function(networks) {
+                               var rv = { };
 
-               runPing6: _luci2.rpc.declare({
-                       object: 'luci2.network',
-                       method: 'ping6',
-                       params: [ 'data' ],
-                       expect: { '': { code: -1 } }
-               }),
+                               var phy_attrs = [
+                                       'country', 'channel', 'frequency', 'frequency_offset',
+                                       'txpower', 'txpower_offset', 'hwmodes', 'hardware', 'phy'
+                               ];
 
-               runTraceroute: _luci2.rpc.declare({
-                       object: 'luci2.network',
-                       method: 'traceroute',
-                       params: [ 'data' ],
-                       expect: { '': { code: -1 } }
-               }),
+                               var net_attrs = [
+                                       'ssid', 'bssid', 'mode', 'quality', 'quality_max',
+                                       'signal', 'noise', 'bitrate', 'encryption'
+                               ];
 
-               runTraceroute6: _luci2.rpc.declare({
-                       object: 'luci2.network',
-                       method: 'traceroute6',
-                       params: [ 'data' ],
-                       expect: { '': { code: -1 } }
-               }),
+                               for (var i = 0; i < networks.length; i++)
+                               {
+                                       var phy = rv[networks[i].phy] || (
+                                               rv[networks[i].phy] = { networks: [ ] }
+                                       );
 
-               runNslookup: _luci2.rpc.declare({
-                       object: 'luci2.network',
-                       method: 'nslookup',
-                       params: [ 'data' ],
-                       expect: { '': { code: -1 } }
-               }),
+                                       var net = {
+                                               device: networks[i].device
+                                       };
 
+                                       for (var j = 0; j < phy_attrs.length; j++)
+                                               phy[phy_attrs[j]] = networks[i][phy_attrs[j]];
 
-               setUp: _luci2.rpc.declare({
-                       object: 'luci2.network',
-                       method: 'ifup',
-                       params: [ 'data' ],
-                       expect: { '': { code: -1 } }
-               }),
+                                       for (var j = 0; j < net_attrs.length; j++)
+                                               net[net_attrs[j]] = networks[i][net_attrs[j]];
 
-               setDown: _luci2.rpc.declare({
-                       object: 'luci2.network',
-                       method: 'ifdown',
-                       params: [ 'data' ],
-                       expect: { '': { code: -1 } }
-               })
-       };
+                                       phy.networks.push(net);
+                               }
 
-       this.wireless = {
-               listDeviceNames: _luci2.rpc.declare({
-                       object: 'iwinfo',
-                       method: 'devices',
-                       expect: { 'devices': [ ] },
-                       filter: function(data) {
-                               data.sort();
-                               return data;
+                               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 = [ ];
+
+                               for (var i = 0; i < assoclists.length; i++)
+                                       for (var j = 0; j < assoclists[i].length; j++)
+                                               rv.push(assoclists[i][j]);
+
+                               return rv;
+                       });
+               },
+
+               formatEncryption: function(enc)
+               {
+                       var format_list = function(l, s)
+                       {
+                               var rv = [ ];
+                               for (var i = 0; i < l.length; i++)
+                                       rv.push(l[i].toUpperCase());
+                               return rv.join(s ? s : ', ');
+                       }
+
+                       if (!enc || !enc.enabled)
+                               return _luci2.tr('None');
+
+                       if (enc.wep)
+                       {
+                               if (enc.wep.length == 2)
+                                       return _luci2.tr('WEP Open/Shared') + ' (%s)'.format(format_list(enc.ciphers, ', '));
+                               else if (enc.wep[0] == 'shared')
+                                       return _luci2.tr('WEP Shared Auth') + ' (%s)'.format(format_list(enc.ciphers, ', '));
+                               else
+                                       return _luci2.tr('WEP Open System') + ' (%s)'.format(format_list(enc.ciphers, ', '));
+                       }
+                       else if (enc.wpa)
+                       {
+                               if (enc.wpa.length == 2)
+                                       return _luci2.tr('mixed WPA/WPA2') + ' %s (%s)'.format(
+                                               format_list(enc.authentication, '/'),
+                                               format_list(enc.ciphers, ', ')
+                                       );
+                               else if (enc.wpa[0] == 2)
+                                       return 'WPA2 %s (%s)'.format(
+                                               format_list(enc.authentication, '/'),
+                                               format_list(enc.ciphers, ', ')
+                                       );
+                               else
+                                       return 'WPA %s (%s)'.format(
+                                               format_list(enc.authentication, '/'),
+                                               format_list(enc.ciphers, ', ')
+                                       );
                        }
+
+                       return _luci2.tr('Unknown');
+               }
+       };
+
+       this.firewall = {
+               getZoneColor: function(zone)
+               {
+                       if ($.isPlainObject(zone))
+                               zone = zone.name;
+
+                       if (zone == 'lan')
+                               return '#90f090';
+                       else if (zone == 'wan')
+                               return '#f09090';
+
+                       for (var i = 0, hash = 0;
+                                i < zone.length;
+                                hash = zone.charCodeAt(i++) + ((hash << 5) - hash));
+
+                       for (var i = 0, color = '#';
+                                i < 3;
+                                color += ('00' + ((hash >> i++ * 8) & 0xFF).tostring(16)).slice(-2));
+
+                       return color;
+               },
+
+               findZoneByNetwork: function(network)
+               {
+                       var self = this;
+                       var zone = undefined;
+
+                       return _luci2.uci.sections('firewall', 'zone', function(z) {
+                               if (!z.name || !z.network)
+                                       return;
+
+                               if (!$.isArray(z.network))
+                                       z.network = z.network.split(/\s+/);
+
+                               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 zone;
+                       });
+               }
+       };
+
+       this.NetworkModel = {
+               _device_blacklist: [
+                       /^gre[0-9]+$/,
+                       /^gretap[0-9]+$/,
+                       /^ifb[0-9]+$/,
+                       /^ip6tnl[0-9]+$/,
+                       /^sit[0-9]+$/,
+                       /^wlan[0-9]+\.sta[0-9]+$/
+               ],
+
+               _cache_functions: [
+                       'protolist', 0, _luci2.rpc.declare({
+                               object: 'network',
+                               method: 'get_proto_handlers',
+                               expect: { '': { } }
+                       }),
+                       'ifstate', 1, _luci2.rpc.declare({
+                               object: 'network.interface',
+                               method: 'dump',
+                               expect: { 'interface': [ ] }
+                       }),
+                       'devstate', 2, _luci2.rpc.declare({
+                               object: 'network.device',
+                               method: 'status',
+                               expect: { '': { } }
+                       }),
+                       'wifistate', 0, _luci2.rpc.declare({
+                               object: 'network.wireless',
+                               method: 'status',
+                               expect: { '': { } }
+                       }),
+                       'bwstate', 2, _luci2.rpc.declare({
+                               object: 'luci2.network.bwmon',
+                               method: 'statistics',
+                               expect: { 'statistics': { } }
+                       }),
+                       'devlist', 2, _luci2.rpc.declare({
+                               object: 'luci2.network',
+                               method: 'device_list',
+                               expect: { 'devices': [ ] }
+                       }),
+                       'swlist', 0, _luci2.rpc.declare({
+                               object: 'luci2.network',
+                               method: 'switch_list',
+                               expect: { 'switches': [ ] }
+                       })
+               ],
+
+               _fetch_protocol: function(proto)
+               {
+                       var url = _luci2.globals.resource + '/proto/' + proto + '.js';
+                       var self = _luci2.NetworkModel;
+
+                       var def = $.Deferred();
+
+                       $.ajax(url, {
+                               method: 'GET',
+                               cache: true,
+                               dataType: 'text'
+                       }).then(function(data) {
+                               try {
+                                       var protoConstructorSource = (
+                                               '(function(L, $) { ' +
+                                                       'return %s' +
+                                               '})(_luci2, $);\n\n' +
+                                               '//@ sourceURL=%s'
+                                       ).format(data, url);
+
+                                       var protoClass = eval(protoConstructorSource);
+
+                                       self._protos[proto] = new protoClass();
+                               }
+                               catch(e) {
+                                       alert('Unable to instantiate proto "%s": %s'.format(url, e));
+                               };
+
+                               def.resolve();
+                       }).fail(function() {
+                               def.resolve();
+                       });
+
+                       return def;
+               },
+
+               _fetch_protocols: function()
+               {
+                       var self = _luci2.NetworkModel;
+                       var deferreds = [ ];
+
+                       for (var proto in self._cache.protolist)
+                               deferreds.push(self._fetch_protocol(proto));
+
+                       return $.when.apply($, deferreds);
+               },
+
+               _fetch_swstate: _luci2.rpc.declare({
+                       object: 'luci2.network',
+                       method: 'switch_info',
+                       params: [ 'switch' ],
+                       expect: { 'info': { } }
                }),
 
-               getDeviceStatus: _luci2.rpc.declare({
-                       object: 'iwinfo',
-                       method: 'info',
-                       params: [ 'device' ],
-                       expect: { '': { } },
-                       filter: function(data, params) {
-                               if (!$.isEmptyObject(data))
+               _fetch_swstate_cb: function(responses) {
+                       var self = _luci2.NetworkModel;
+                       var swlist = self._cache.swlist;
+                       var swstate = self._cache.swstate = { };
+
+                       for (var i = 0; i < responses.length; i++)
+                               swstate[swlist[i]] = responses[i];
+               },
+
+               _fetch_cache_cb: function(level)
+               {
+                       var self = _luci2.NetworkModel;
+                       var name = '_fetch_cache_cb_' + level;
+
+                       return self[name] || (
+                               self[name] = function(responses)
+                               {
+                                       for (var i = 0; i < self._cache_functions.length; i += 3)
+                                               if (!level || self._cache_functions[i + 1] == level)
+                                                       self._cache[self._cache_functions[i]] = responses.shift();
+
+                                       if (!level)
+                                       {
+                                               _luci2.rpc.batch();
+
+                                               for (var i = 0; i < self._cache.swlist.length; i++)
+                                                       self._fetch_swstate(self._cache.swlist[i]);
+
+                                               return _luci2.rpc.flush().then(self._fetch_swstate_cb);
+                                       }
+
+                                       return _luci2.deferrable();
+                               }
+                       );
+               },
+
+               _fetch_cache: function(level)
+               {
+                       var self = _luci2.NetworkModel;
+
+                       return _luci2.uci.load(['network', 'wireless']).then(function() {
+                               _luci2.rpc.batch();
+
+                               for (var i = 0; i < self._cache_functions.length; i += 3)
+                                       if (!level || self._cache_functions[i + 1] == level)
+                                               self._cache_functions[i + 2]();
+
+                               return _luci2.rpc.flush().then(self._fetch_cache_cb(level || 0));
+                       });
+               },
+
+               _get: function(pkg, sid, key)
+               {
+                       return _luci2.uci.get(pkg, sid, key);
+               },
+
+               _set: function(pkg, sid, key, val)
+               {
+                       return _luci2.uci.set(pkg, sid, key, val);
+               },
+
+               _is_blacklisted: function(dev)
+               {
+                       for (var i = 0; i < this._device_blacklist.length; i++)
+                               if (dev.match(this._device_blacklist[i]))
+                                       return true;
+
+                       return false;
+               },
+
+               _sort_devices: function(a, b)
+               {
+                       if (a.options.kind < b.options.kind)
+                               return -1;
+                       else if (a.options.kind > b.options.kind)
+                               return 1;
+
+                       if (a.options.name < b.options.name)
+                               return -1;
+                       else if (a.options.name > b.options.name)
+                               return 1;
+
+                       return 0;
+               },
+
+               _get_dev: function(ifname)
+               {
+                       var alias = (ifname.charAt(0) == '@');
+                       return this._devs[ifname] || (
+                               this._devs[ifname] = {
+                                       ifname:  ifname,
+                                       kind:    alias ? 'alias' : 'ethernet',
+                                       type:    alias ? 0 : 1,
+                                       up:      false,
+                                       changed: { }
+                               }
+                       );
+               },
+
+               _get_iface: function(name)
+               {
+                       return this._ifaces[name] || (
+                               this._ifaces[name] = {
+                                       name:    name,
+                                       proto:   this._protos.none,
+                                       changed: { }
+                               }
+                       );
+               },
+
+               _parse_devices: function()
+               {
+                       var self = _luci2.NetworkModel;
+                       var wificount = { };
+
+                       for (var ifname in self._cache.devstate)
+                       {
+                               if (self._is_blacklisted(ifname))
+                                       continue;
+
+                               var dev = self._cache.devstate[ifname];
+                               var entry = self._get_dev(ifname);
+
+                               entry.up = dev.up;
+
+                               switch (dev.type)
+                               {
+                               case 'IP tunnel':
+                                       entry.kind = 'tunnel';
+                                       break;
+
+                               case 'Bridge':
+                                       entry.kind = 'bridge';
+                                       //entry.ports = dev['bridge-members'].sort();
+                                       break;
+                               }
+                       }
+
+                       for (var i = 0; i < self._cache.devlist.length; i++)
+                       {
+                               var dev = self._cache.devlist[i];
+
+                               if (self._is_blacklisted(dev.device))
+                                       continue;
+
+                               var entry = self._get_dev(dev.device);
+
+                               entry.up   = dev.is_up;
+                               entry.type = dev.type;
+
+                               switch (dev.type)
+                               {
+                               case 1: /* Ethernet */
+                                       if (dev.is_bridge)
+                                               entry.kind = 'bridge';
+                                       else if (dev.is_tuntap)
+                                               entry.kind = 'tunnel';
+                                       else if (dev.is_wireless)
+                                               entry.kind = 'wifi';
+                                       break;
+
+                               case 512: /* PPP */
+                               case 768: /* IP-IP Tunnel */
+                               case 769: /* IP6-IP6 Tunnel */
+                               case 776: /* IPv6-in-IPv4 */
+                               case 778: /* GRE over IP */
+                                       entry.kind = 'tunnel';
+                                       break;
+                               }
+                       }
+
+                       var net = _luci2.uci.sections('network');
+                       for (var i = 0; i < net.length; i++)
+                       {
+                               var s = net[i];
+                               var sid = s['.name'];
+
+                               if (s['.type'] == 'device' && s.name)
+                               {
+                                       var entry = self._get_dev(s.name);
+
+                                       switch (s.type)
+                                       {
+                                       case 'macvlan':
+                                       case 'tunnel':
+                                               entry.kind = 'tunnel';
+                                               break;
+                                       }
+
+                                       entry.sid = sid;
+                               }
+                               else if (s['.type'] == 'interface' && !s['.anonymous'] && s.ifname)
+                               {
+                                       var ifnames = _luci2.toArray(s.ifname);
+
+                                       for (var j = 0; j < ifnames.length; j++)
+                                               self._get_dev(ifnames[j]);
+
+                                       if (s['.name'] != 'loopback')
+                                       {
+                                               var entry = self._get_dev('@%s'.format(s['.name']));
+
+                                               entry.type = 0;
+                                               entry.kind = 'alias';
+                                               entry.sid  = sid;
+                                       }
+                               }
+                               else if (s['.type'] == 'switch_vlan' && s.device)
+                               {
+                                       var sw = self._cache.swstate[s.device];
+                                       var vid = parseInt(s.vid || s.vlan);
+                                       var ports = _luci2.toArray(s.ports);
+
+                                       if (!sw || !ports.length || isNaN(vid))
+                                               continue;
+
+                                       var ifname = undefined;
+
+                                       for (var j = 0; j < ports.length; j++)
+                                       {
+                                               var port = parseInt(ports[j]);
+                                               var tag = (ports[j].replace(/[^tu]/g, '') == 't');
+
+                                               if (port == sw.cpu_port)
+                                               {
+                                                       // XXX: need a way to map switch to netdev
+                                                       if (tag)
+                                                               ifname = 'eth0.%d'.format(vid);
+                                                       else
+                                                               ifname = 'eth0';
+
+                                                       break;
+                                               }
+                                       }
+
+                                       if (!ifname)
+                                               continue;
+
+                                       var entry = self._get_dev(ifname);
+
+                                       entry.kind = 'vlan';
+                                       entry.sid  = sid;
+                                       entry.vsw  = sw;
+                                       entry.vid  = vid;
+                               }
+                       }
+
+                       var wifi = _luci2.uci.sections('wireless');
+                       for (var i = 0; i < wifi.length; i++)
+                       {
+                               var s = wifi[i];
+                               var sid = s['.name'];
+
+                               if (s['.type'] == 'wifi-iface' && s.device)
+                               {
+                                       var r = parseInt(s.device.replace(/^[^0-9]+/, ''));
+                                       var n = wificount[s.device] = (wificount[s.device] || 0) + 1;
+                                       var id = 'radio%d.network%d'.format(r, n);
+                                       var ifname = id;
+
+                                       if (self._cache.wifistate[s.device])
+                                       {
+                                               var ifcs = self._cache.wifistate[s.device].interfaces;
+                                               for (var ifc in ifcs)
+                                               {
+                                                       if (ifcs[ifc].section == sid)
+                                                       {
+                                                               ifname = ifcs[ifc].ifname;
+                                                               break;
+                                                       }
+                                               }
+                                       }
+
+                                       var entry = self._get_dev(ifname);
+
+                                       entry.kind   = 'wifi';
+                                       entry.sid    = sid;
+                                       entry.wid    = id;
+                                       entry.wdev   = s.device;
+                                       entry.wmode  = s.mode;
+                                       entry.wssid  = s.ssid;
+                                       entry.wbssid = s.bssid;
+                               }
+                       }
+
+                       for (var i = 0; i < net.length; i++)
+                       {
+                               var s = net[i];
+                               var sid = s['.name'];
+
+                               if (s['.type'] == 'interface' && !s['.anonymous'] && s.type == 'bridge')
+                               {
+                                       var ifnames = _luci2.toArray(s.ifname);
+
+                                       for (var ifname in self._devs)
+                                       {
+                                               var dev = self._devs[ifname];
+
+                                               if (dev.kind != 'wifi')
+                                                       continue;
+
+                                               var wnets = _luci2.toArray(_luci2.uci.get('wireless', dev.sid, 'network'));
+                                               if ($.inArray(sid, wnets) > -1)
+                                                       ifnames.push(ifname);
+                                       }
+
+                                       entry = self._get_dev('br-%s'.format(s['.name']));
+                                       entry.type  = 1;
+                                       entry.kind  = 'bridge';
+                                       entry.sid   = sid;
+                                       entry.ports = ifnames.sort();
+                               }
+                       }
+               },
+
+               _parse_interfaces: function()
+               {
+                       var self = _luci2.NetworkModel;
+                       var net = _luci2.uci.sections('network');
+
+                       for (var i = 0; i < net.length; i++)
+                       {
+                               var s = net[i];
+                               var sid = s['.name'];
+
+                               if (s['.type'] == 'interface' && !s['.anonymous'] && s.proto)
+                               {
+                                       var entry = self._get_iface(s['.name']);
+                                       var proto = self._protos[s.proto] || self._protos.none;
+
+                                       var l3dev = undefined;
+                                       var l2dev = undefined;
+
+                                       var ifnames = _luci2.toArray(s.ifname);
+
+                                       for (var ifname in self._devs)
+                                       {
+                                               var dev = self._devs[ifname];
+
+                                               if (dev.kind != 'wifi')
+                                                       continue;
+
+                                               var wnets = _luci2.toArray(_luci2.uci.get('wireless', dev.sid, 'network'));
+                                               if ($.inArray(entry.name, wnets) > -1)
+                                                       ifnames.push(ifname);
+                                       }
+
+                                       if (proto.virtual)
+                                               l3dev = '%s-%s'.format(s.proto, entry.name);
+                                       else if (s.type == 'bridge')
+                                               l3dev = 'br-%s'.format(entry.name);
+                                       else
+                                               l3dev = ifnames[0];
+
+                                       if (!proto.virtual && s.type == 'bridge')
+                                               l2dev = 'br-%s'.format(entry.name);
+                                       else if (!proto.virtual)
+                                               l2dev = ifnames[0];
+
+                                       entry.proto = proto;
+                                       entry.sid   = sid;
+                                       entry.l3dev = l3dev;
+                                       entry.l2dev = l2dev;
+                               }
+                       }
+
+                       for (var i = 0; i < self._cache.ifstate.length; i++)
+                       {
+                               var iface = self._cache.ifstate[i];
+                               var entry = self._get_iface(iface['interface']);
+                               var proto = self._protos[iface.proto] || self._protos.none;
+
+                               /* this is a virtual interface, either deleted from config but
+                                  not applied yet or set up from external tools (6rd) */
+                               if (!entry.sid)
+                               {
+                                       entry.proto = proto;
+                                       entry.l2dev = iface.device;
+                                       entry.l3dev = iface.l3_device;
+                               }
+                       }
+               },
+
+               init: function()
+               {
+                       var self = this;
+
+                       if (self._cache)
+                               return _luci2.deferrable();
+
+                       self._cache  = { };
+                       self._devs   = { };
+                       self._ifaces = { };
+                       self._protos = { };
+
+                       return self._fetch_cache()
+                               .then(self._fetch_protocols)
+                               .then(self._parse_devices)
+                               .then(self._parse_interfaces);
+               },
+
+               update: function()
+               {
+                       delete this._cache;
+                       return this.init();
+               },
+
+               refreshInterfaceStatus: function()
+               {
+                       return this._fetch_cache(1).then(this._parse_interfaces);
+               },
+
+               refreshDeviceStatus: function()
+               {
+                       return this._fetch_cache(2).then(this._parse_devices);
+               },
+
+               refreshStatus: function()
+               {
+                       return this._fetch_cache(1)
+                               .then(this._fetch_cache(2))
+                               .then(this._parse_devices)
+                               .then(this._parse_interfaces);
+               },
+
+               getDevices: function()
+               {
+                       var devs = [ ];
+
+                       for (var ifname in this._devs)
+                               if (ifname != 'lo')
+                                       devs.push(new _luci2.NetworkModel.Device(this._devs[ifname]));
+
+                       return devs.sort(this._sort_devices);
+               },
+
+               getDeviceByInterface: function(iface)
+               {
+                       if (iface instanceof _luci2.NetworkModel.Interface)
+                               iface = iface.name();
+
+                       if (this._ifaces[iface])
+                               return this.getDevice(this._ifaces[iface].l3dev) ||
+                                      this.getDevice(this._ifaces[iface].l2dev);
+
+                       return undefined;
+               },
+
+               getDevice: function(ifname)
+               {
+                       if (this._devs[ifname])
+                               return new _luci2.NetworkModel.Device(this._devs[ifname]);
+
+                       return undefined;
+               },
+
+               createDevice: function(name)
+               {
+                       return new _luci2.NetworkModel.Device(this._get_dev(name));
+               },
+
+               getInterfaces: function()
+               {
+                       var ifaces = [ ];
+
+                       for (var name in this._ifaces)
+                               if (name != 'loopback')
+                                       ifaces.push(this.getInterface(name));
+
+                       ifaces.sort(function(a, b) {
+                               if (a.name() < b.name())
+                                       return -1;
+                               else if (a.name() > b.name())
+                                       return 1;
+                               else
+                                       return 0;
+                       });
+
+                       return ifaces;
+               },
+
+               getInterfacesByDevice: function(dev)
+               {
+                       var ifaces = [ ];
+
+                       if (dev instanceof _luci2.NetworkModel.Device)
+                               dev = dev.name();
+
+                       for (var name in this._ifaces)
+                       {
+                               var iface = this._ifaces[name];
+                               if (iface.l2dev == dev || iface.l3dev == dev)
+                                       ifaces.push(this.getInterface(name));
+                       }
+
+                       ifaces.sort(function(a, b) {
+                               if (a.name() < b.name())
+                                       return -1;
+                               else if (a.name() > b.name())
+                                       return 1;
+                               else
+                                       return 0;
+                       });
+
+                       return ifaces;
+               },
+
+               getInterface: function(iface)
+               {
+                       if (this._ifaces[iface])
+                               return new _luci2.NetworkModel.Interface(this._ifaces[iface]);
+
+                       return undefined;
+               },
+
+               getProtocols: function()
+               {
+                       var rv = [ ];
+
+                       for (var proto in this._protos)
+                       {
+                               var pr = this._protos[proto];
+
+                               rv.push({
+                                       name:        proto,
+                                       description: pr.description,
+                                       virtual:     pr.virtual,
+                                       tunnel:      pr.tunnel
+                               });
+                       }
+
+                       return rv.sort(function(a, b) {
+                               if (a.name < b.name)
+                                       return -1;
+                               else if (a.name > b.name)
+                                       return 1;
+                               else
+                                       return 0;
+                       });
+               },
+
+               _find_wan: function(ipaddr)
+               {
+                       for (var i = 0; i < this._cache.ifstate.length; i++)
+                       {
+                               var ifstate = this._cache.ifstate[i];
+
+                               if (!ifstate.route)
+                                       continue;
+
+                               for (var j = 0; j < ifstate.route.length; j++)
+                                       if (ifstate.route[j].mask == 0 &&
+                                           ifstate.route[j].target == ipaddr &&
+                                           typeof(ifstate.route[j].table) == 'undefined')
+                                       {
+                                               return this.getInterface(ifstate['interface']);
+                                       }
+                       }
+
+                       return undefined;
+               },
+
+               findWAN: function()
+               {
+                       return this._find_wan('0.0.0.0');
+               },
+
+               findWAN6: function()
+               {
+                       return this._find_wan('::');
+               },
+
+               resolveAlias: function(ifname)
+               {
+                       if (ifname instanceof _luci2.NetworkModel.Device)
+                               ifname = ifname.name();
+
+                       var dev = this._devs[ifname];
+                       var seen = { };
+
+                       while (dev && dev.kind == 'alias')
+                       {
+                               // loop
+                               if (seen[dev.ifname])
+                                       return undefined;
+
+                               var ifc = this._ifaces[dev.sid];
+
+                               seen[dev.ifname] = true;
+                               dev = ifc ? this._devs[ifc.l3dev] : undefined;
+                       }
+
+                       return dev ? this.getDevice(dev.ifname) : undefined;
+               }
+       };
+
+       this.NetworkModel.Device = Class.extend({
+               _wifi_modes: {
+                       ap: _luci2.tr('Master'),
+                       sta: _luci2.tr('Client'),
+                       adhoc: _luci2.tr('Ad-Hoc'),
+                       monitor: _luci2.tr('Monitor'),
+                       wds: _luci2.tr('Static WDS')
+               },
+
+               _status: function(key)
+               {
+                       var s = _luci2.NetworkModel._cache.devstate[this.options.ifname];
+
+                       if (s)
+                               return key ? s[key] : s;
+
+                       return undefined;
+               },
+
+               get: function(key)
+               {
+                       var sid = this.options.sid;
+                       var pkg = (this.options.kind == 'wifi') ? 'wireless' : 'network';
+                       return _luci2.NetworkModel._get(pkg, sid, key);
+               },
+
+               set: function(key, val)
+               {
+                       var sid = this.options.sid;
+                       var pkg = (this.options.kind == 'wifi') ? 'wireless' : 'network';
+                       return _luci2.NetworkModel._set(pkg, sid, key, val);
+               },
+
+               init: function()
+               {
+                       if (typeof(this.options.type) == 'undefined')
+                               this.options.type = 1;
+
+                       if (typeof(this.options.kind) == 'undefined')
+                               this.options.kind = 'ethernet';
+
+                       if (typeof(this.options.networks) == 'undefined')
+                               this.options.networks = [ ];
+               },
+
+               name: function()
+               {
+                       return this.options.ifname;
+               },
+
+               description: function()
+               {
+                       switch (this.options.kind)
+                       {
+                       case 'alias':
+                               return _luci2.tr('Alias for network "%s"').format(this.options.ifname.substring(1));
+
+                       case 'bridge':
+                               return _luci2.tr('Network bridge');
+
+                       case 'ethernet':
+                               return _luci2.tr('Network device');
+
+                       case 'tunnel':
+                               switch (this.options.type)
+                               {
+                               case 1: /* tuntap */
+                                       return _luci2.tr('TAP device');
+
+                               case 512: /* PPP */
+                                       return _luci2.tr('PPP tunnel');
+
+                               case 768: /* IP-IP Tunnel */
+                                       return _luci2.tr('IP-in-IP tunnel');
+
+                               case 769: /* IP6-IP6 Tunnel */
+                                       return _luci2.tr('IPv6-in-IPv6 tunnel');
+
+                               case 776: /* IPv6-in-IPv4 */
+                                       return _luci2.tr('IPv6-over-IPv4 tunnel');
+                                       break;
+
+                               case 778: /* GRE over IP */
+                                       return _luci2.tr('GRE-over-IP tunnel');
+
+                               default:
+                                       return _luci2.tr('Tunnel device');
+                               }
+
+                       case 'vlan':
+                               return _luci2.tr('VLAN %d on %s').format(this.options.vid, this.options.vsw.model);
+
+                       case 'wifi':
+                               var o = this.options;
+                               return _luci2.trc('(Wifi-Mode) "(SSID)" on (radioX)', '%s "%h" on %s').format(
+                                       o.wmode ? this._wifi_modes[o.wmode] : _luci2.tr('Unknown mode'),
+                                       o.wssid || '?', o.wdev
+                               );
+                       }
+
+                       return _luci2.tr('Unknown device');
+               },
+
+               icon: function(up)
+               {
+                       var kind = this.options.kind;
+
+                       if (kind == 'alias')
+                               kind = 'ethernet';
+
+                       if (typeof(up) == 'undefined')
+                               up = this.isUp();
+
+                       return _luci2.globals.resource + '/icons/%s%s.png'.format(kind, up ? '' : '_disabled');
+               },
+
+               isUp: function()
+               {
+                       var l = _luci2.NetworkModel._cache.devlist;
+
+                       for (var i = 0; i < l.length; i++)
+                               if (l[i].device == this.options.ifname)
+                                       return (l[i].is_up === true);
+
+                       return false;
+               },
+
+               isAlias: function()
+               {
+                       return (this.options.kind == 'alias');
+               },
+
+               isBridge: function()
+               {
+                       return (this.options.kind == 'bridge');
+               },
+
+               isBridgeable: function()
+               {
+                       return (this.options.type == 1 && this.options.kind != 'bridge');
+               },
+
+               isWireless: function()
+               {
+                       return (this.options.kind == 'wifi');
+               },
+
+               isInNetwork: function(net)
+               {
+                       if (!(net instanceof _luci2.NetworkModel.Interface))
+                               net = _luci2.NetworkModel.getInterface(net);
+
+                       if (net)
+                       {
+                               if (net.options.l3dev == this.options.ifname ||
+                                   net.options.l2dev == this.options.ifname)
+                                       return true;
+
+                               var dev = _luci2.NetworkModel._devs[net.options.l2dev];
+                               if (dev && dev.kind == 'bridge' && dev.ports)
+                                       return ($.inArray(this.options.ifname, dev.ports) > -1);
+                       }
+
+                       return false;
+               },
+
+               getMTU: function()
+               {
+                       var dev = _luci2.NetworkModel._cache.devstate[this.options.ifname];
+                       if (dev && !isNaN(dev.mtu))
+                               return dev.mtu;
+
+                       return undefined;
+               },
+
+               getMACAddress: function()
+               {
+                       if (this.options.type != 1)
+                               return undefined;
+
+                       var dev = _luci2.NetworkModel._cache.devstate[this.options.ifname];
+                       if (dev && dev.macaddr)
+                               return dev.macaddr.toUpperCase();
+
+                       return undefined;
+               },
+
+               getInterfaces: function()
+               {
+                       return _luci2.NetworkModel.getInterfacesByDevice(this.options.name);
+               },
+
+               getStatistics: function()
+               {
+                       var s = this._status('statistics') || { };
+                       return {
+                               rx_bytes: (s.rx_bytes || 0),
+                               tx_bytes: (s.tx_bytes || 0),
+                               rx_packets: (s.rx_packets || 0),
+                               tx_packets: (s.tx_packets || 0)
+                       };
+               },
+
+               getTrafficHistory: function()
+               {
+                       var def = new Array(120);
+
+                       for (var i = 0; i < 120; i++)
+                               def[i] = 0;
+
+                       var h = _luci2.NetworkModel._cache.bwstate[this.options.ifname] || { };
+                       return {
+                               rx_bytes: (h.rx_bytes || def),
+                               tx_bytes: (h.tx_bytes || def),
+                               rx_packets: (h.rx_packets || def),
+                               tx_packets: (h.tx_packets || def)
+                       };
+               },
+
+               removeFromInterface: function(iface)
+               {
+                       if (!(iface instanceof _luci2.NetworkModel.Interface))
+                               iface = _luci2.NetworkModel.getInterface(iface);
+
+                       if (!iface)
+                               return;
+
+                       var ifnames = _luci2.toArray(iface.get('ifname'));
+                       if ($.inArray(this.options.ifname, ifnames) > -1)
+                               iface.set('ifname', _luci2.filterArray(ifnames, this.options.ifname));
+
+                       if (this.options.kind != 'wifi')
+                               return;
+
+                       var networks = _luci2.toArray(this.get('network'));
+                       if ($.inArray(iface.name(), networks) > -1)
+                               this.set('network', _luci2.filterArray(networks, iface.name()));
+               },
+
+               attachToInterface: function(iface)
+               {
+                       if (!(iface instanceof _luci2.NetworkModel.Interface))
+                               iface = _luci2.NetworkModel.getInterface(iface);
+
+                       if (!iface)
+                               return;
+
+                       if (this.options.kind != 'wifi')
+                       {
+                               var ifnames = _luci2.toArray(iface.get('ifname'));
+                               if ($.inArray(this.options.ifname, ifnames) < 0)
+                               {
+                                       ifnames.push(this.options.ifname);
+                                       iface.set('ifname', (ifnames.length > 1) ? ifnames : ifnames[0]);
+                               }
+                       }
+                       else
+                       {
+                               var networks = _luci2.toArray(this.get('network'));
+                               if ($.inArray(iface.name(), networks) < 0)
+                               {
+                                       networks.push(iface.name());
+                                       this.set('network', (networks.length > 1) ? networks : networks[0]);
+                               }
+                       }
+               }
+       });
+
+       this.NetworkModel.Interface = Class.extend({
+               _status: function(key)
+               {
+                       var s = _luci2.NetworkModel._cache.ifstate;
+
+                       for (var i = 0; i < s.length; i++)
+                               if (s[i]['interface'] == this.options.name)
+                                       return key ? s[i][key] : s[i];
+
+                       return undefined;
+               },
+
+               get: function(key)
+               {
+                       return _luci2.NetworkModel._get('network', this.options.name, key);
+               },
+
+               set: function(key, val)
+               {
+                       return _luci2.NetworkModel._set('network', this.options.name, key, val);
+               },
+
+               name: function()
+               {
+                       return this.options.name;
+               },
+
+               protocol: function()
+               {
+                       return (this.get('proto') || 'none');
+               },
+
+               isUp: function()
+               {
+                       return (this._status('up') === true);
+               },
+
+               isVirtual: function()
+               {
+                       return (typeof(this.options.sid) != 'string');
+               },
+
+               getProtocol: function()
+               {
+                       var prname = this.get('proto') || 'none';
+                       return _luci2.NetworkModel._protos[prname] || _luci2.NetworkModel._protos.none;
+               },
+
+               getUptime: function()
+               {
+                       var uptime = this._status('uptime');
+                       return isNaN(uptime) ? 0 : uptime;
+               },
+
+               getDevice: function(resolveAlias)
+               {
+                       if (this.options.l3dev)
+                               return _luci2.NetworkModel.getDevice(this.options.l3dev);
+
+                       return undefined;
+               },
+
+               getPhysdev: function()
+               {
+                       if (this.options.l2dev)
+                               return _luci2.NetworkModel.getDevice(this.options.l2dev);
+
+                       return undefined;
+               },
+
+               getSubdevices: function()
+               {
+                       var rv = [ ];
+                       var dev = this.options.l2dev ?
+                               _luci2.NetworkModel._devs[this.options.l2dev] : undefined;
+
+                       if (dev && dev.kind == 'bridge' && dev.ports && dev.ports.length)
+                               for (var i = 0; i < dev.ports.length; i++)
+                                       rv.push(_luci2.NetworkModel.getDevice(dev.ports[i]));
+
+                       return rv;
+               },
+
+               getIPv4Addrs: function(mask)
+               {
+                       var rv = [ ];
+                       var addrs = this._status('ipv4-address');
+
+                       if (addrs)
+                               for (var i = 0; i < addrs.length; i++)
+                                       if (!mask)
+                                               rv.push(addrs[i].address);
+                                       else
+                                               rv.push('%s/%d'.format(addrs[i].address, addrs[i].mask));
+
+                       return rv;
+               },
+
+               getIPv6Addrs: function(mask)
+               {
+                       var rv = [ ];
+                       var addrs;
+
+                       addrs = this._status('ipv6-address');
+
+                       if (addrs)
+                               for (var i = 0; i < addrs.length; i++)
+                                       if (!mask)
+                                               rv.push(addrs[i].address);
+                                       else
+                                               rv.push('%s/%d'.format(addrs[i].address, addrs[i].mask));
+
+                       addrs = this._status('ipv6-prefix-assignment');
+
+                       if (addrs)
+                               for (var i = 0; i < addrs.length; i++)
+                                       if (!mask)
+                                               rv.push('%s1'.format(addrs[i].address));
+                                       else
+                                               rv.push('%s1/%d'.format(addrs[i].address, addrs[i].mask));
+
+                       return rv;
+               },
+
+               getDNSAddrs: function()
+               {
+                       var rv = [ ];
+                       var addrs = this._status('dns-server');
+
+                       if (addrs)
+                               for (var i = 0; i < addrs.length; i++)
+                                       rv.push(addrs[i]);
+
+                       return rv;
+               },
+
+               getIPv4DNS: function()
+               {
+                       var rv = [ ];
+                       var dns = this._status('dns-server');
+
+                       if (dns)
+                               for (var i = 0; i < dns.length; i++)
+                                       if (dns[i].indexOf(':') == -1)
+                                               rv.push(dns[i]);
+
+                       return rv;
+               },
+
+               getIPv6DNS: function()
+               {
+                       var rv = [ ];
+                       var dns = this._status('dns-server');
+
+                       if (dns)
+                               for (var i = 0; i < dns.length; i++)
+                                       if (dns[i].indexOf(':') > -1)
+                                               rv.push(dns[i]);
+
+                       return rv;
+               },
+
+               getIPv4Gateway: function()
+               {
+                       var rt = this._status('route');
+
+                       if (rt)
+                               for (var i = 0; i < rt.length; i++)
+                                       if (rt[i].target == '0.0.0.0' && rt[i].mask == 0)
+                                               return rt[i].nexthop;
+
+                       return undefined;
+               },
+
+               getIPv6Gateway: function()
+               {
+                       var rt = this._status('route');
+
+                       if (rt)
+                               for (var i = 0; i < rt.length; i++)
+                                       if (rt[i].target == '::' && rt[i].mask == 0)
+                                               return rt[i].nexthop;
+
+                       return undefined;
+               },
+
+               getStatistics: function()
+               {
+                       var dev = this.getDevice() || new _luci2.NetworkModel.Device({});
+                       return dev.getStatistics();
+               },
+
+               getTrafficHistory: function()
+               {
+                       var dev = this.getDevice() || new _luci2.NetworkModel.Device({});
+                       return dev.getTrafficHistory();
+               },
+
+               setDevices: function(devs)
+               {
+                       var dev = this.getPhysdev();
+                       var old_devs = [ ];
+                       var changed = false;
+
+                       if (dev && dev.isBridge())
+                               old_devs = this.getSubdevices();
+                       else if (dev)
+                               old_devs = [ dev ];
+
+                       if (old_devs.length != devs.length)
+                               changed = true;
+                       else
+                               for (var i = 0; i < old_devs.length; i++)
+                               {
+                                       var dev = devs[i];
+
+                                       if (dev instanceof _luci2.NetworkModel.Device)
+                                               dev = dev.name();
+
+                                       if (!dev || old_devs[i].name() != dev)
+                                       {
+                                               changed = true;
+                                               break;
+                                       }
+                               }
+
+                       if (changed)
+                       {
+                               for (var i = 0; i < old_devs.length; i++)
+                                       old_devs[i].removeFromInterface(this);
+
+                               for (var i = 0; i < devs.length; i++)
                                {
-                                       data['device'] = params['device'];
-                                       return data;
+                                       var dev = devs[i];
+
+                                       if (!(dev instanceof _luci2.NetworkModel.Device))
+                                               dev = _luci2.NetworkModel.getDevice(dev);
+
+                                       if (dev)
+                                               dev.attachToInterface(this);
                                }
-                               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'];
+               changeProtocol: function(proto)
+               {
+                       var pr = _luci2.NetworkModel._protos[proto];
 
-                               data.sort(function(a, b) {
-                                       if (a.bssid < b.bssid)
-                                               return -1;
-                                       else if (a.bssid > b.bssid)
-                                               return 1;
-                                       else
-                                               return 0;
-                               });
+                       if (!pr)
+                               return;
 
-                               return data;
-                       }
-               }),
+                       for (var opt in (this.get() || { }))
+                       {
+                               switch (opt)
+                               {
+                               case 'type':
+                               case 'ifname':
+                               case 'macaddr':
+                                       if (pr.virtual)
+                                               this.set(opt, undefined);
+                                       break;
 
-               getWirelessStatus: function() {
-                       return this.listDeviceNames().then(function(names) {
-                               _luci2.rpc.batch();
+                               case 'auto':
+                               case 'mtu':
+                                       break;
 
-                               for (var i = 0; i < names.length; i++)
-                                       _luci2.wireless.getDeviceStatus(names[i]);
+                               case 'proto':
+                                       this.set(opt, pr.protocol);
+                                       break;
 
-                               return _luci2.rpc.flush();
-                       }).then(function(networks) {
-                               var rv = { };
+                               default:
+                                       this.set(opt, undefined);
+                                       break;
+                               }
+                       }
+               },
 
-                               var phy_attrs = [
-                                       'country', 'channel', 'frequency', 'frequency_offset',
-                                       'txpower', 'txpower_offset', 'hwmodes', 'hardware', 'phy'
-                               ];
+               createForm: function(mapwidget)
+               {
+                       var self = this;
+                       var proto = self.getProtocol();
+                       var device = self.getDevice();
 
-                               var net_attrs = [
-                                       'ssid', 'bssid', 'mode', 'quality', 'quality_max',
-                                       'signal', 'noise', 'bitrate', 'encryption'
-                               ];
+                       if (!mapwidget)
+                               mapwidget = _luci2.cbi.Map;
 
-                               for (var i = 0; i < networks.length; i++)
-                               {
-                                       var phy = rv[networks[i].phy] || (
-                                               rv[networks[i].phy] = { networks: [ ] }
-                                       );
+                       var map = new mapwidget('network', {
+                               caption:     _luci2.tr('Configure "%s"').format(self.name())
+                       });
 
-                                       var net = {
-                                               device: networks[i].device
-                                       };
+                       var section = map.section(_luci2.cbi.SingleSection, self.name(), {
+                               anonymous:   true
+                       });
 
-                                       for (var j = 0; j < phy_attrs.length; j++)
-                                               phy[phy_attrs[j]] = networks[i][phy_attrs[j]];
+                       section.tab({
+                               id:      'general',
+                               caption: _luci2.tr('General Settings')
+                       });
 
-                                       for (var j = 0; j < net_attrs.length; j++)
-                                               net[net_attrs[j]] = networks[i][net_attrs[j]];
+                       section.tab({
+                               id:      'advanced',
+                               caption: _luci2.tr('Advanced Settings')
+                       });
 
-                                       phy.networks.push(net);
-                               }
+                       section.tab({
+                               id:      'ipv6',
+                               caption: _luci2.tr('IPv6')
+                       });
 
-                               return rv;
+                       section.tab({
+                               id:      'physical',
+                               caption: _luci2.tr('Physical Settings')
                        });
-               },
 
-               getAssocLists: function()
-               {
-                       return this.listDeviceNames().then(function(names) {
-                               _luci2.rpc.batch();
 
-                               for (var i = 0; i < names.length; i++)
-                                       _luci2.wireless.getAssocList(names[i]);
+                       section.taboption('general', _luci2.cbi.CheckboxValue, 'auto', {
+                               caption:     _luci2.tr('Start on boot'),
+                               optional:    true,
+                               initial:     true
+                       });
 
-                               return _luci2.rpc.flush();
-                       }).then(function(assoclists) {
-                               var rv = [ ];
+                       var pr = section.taboption('general', _luci2.cbi.ListValue, 'proto', {
+                               caption:     _luci2.tr('Protocol')
+                       });
 
-                               for (var i = 0; i < assoclists.length; i++)
-                                       for (var j = 0; j < assoclists[i].length; j++)
-                                               rv.push(assoclists[i][j]);
+                       pr.ucivalue = function(sid) {
+                               return self.get('proto') || 'none';
+                       };
 
-                               return rv;
+                       var ok = section.taboption('general', _luci2.cbi.ButtonValue, '_confirm', {
+                               caption:     _luci2.tr('Really switch?'),
+                               description: _luci2.tr('Changing the protocol will clear all configuration for this interface!'),
+                               text:        _luci2.tr('Change protocol')
                        });
-               },
 
-               formatEncryption: function(enc)
-               {
-                       var format_list = function(l, s)
-                       {
-                               var rv = [ ];
-                               for (var i = 0; i < l.length; i++)
-                                       rv.push(l[i].toUpperCase());
-                               return rv.join(s ? s : ', ');
-                       }
+                       ok.on('click', function(ev) {
+                               self.changeProtocol(pr.formvalue(ev.data.sid));
+                               self.createForm(mapwidget).show();
+                       });
 
-                       if (!enc || !enc.enabled)
-                               return _luci2.tr('None');
+                       var protos = _luci2.NetworkModel.getProtocols();
 
-                       if (enc.wep)
-                       {
-                               if (enc.wep.length == 2)
-                                       return _luci2.tr('WEP Open/Shared') + ' (%s)'.format(format_list(enc.ciphers, ', '));
-                               else if (enc.wep[0] == 'shared')
-                                       return _luci2.tr('WEP Shared Auth') + ' (%s)'.format(format_list(enc.ciphers, ', '));
-                               else
-                                       return _luci2.tr('WEP Open System') + ' (%s)'.format(format_list(enc.ciphers, ', '));
-                       }
-                       else if (enc.wpa)
-                       {
-                               if (enc.wpa.length == 2)
-                                       return _luci2.tr('mixed WPA/WPA2') + ' %s (%s)'.format(
-                                               format_list(enc.authentication, '/'),
-                                               format_list(enc.ciphers, ', ')
-                                       );
-                               else if (enc.wpa[0] == 2)
-                                       return 'WPA2 %s (%s)'.format(
-                                               format_list(enc.authentication, '/'),
-                                               format_list(enc.ciphers, ', ')
-                                       );
-                               else
-                                       return 'WPA %s (%s)'.format(
-                                               format_list(enc.authentication, '/'),
-                                               format_list(enc.ciphers, ', ')
-                                       );
-                       }
+                       for (var i = 0; i < protos.length; i++)
+                               pr.value(protos[i].name, protos[i].description);
 
-                       return _luci2.tr('Unknown');
-               }
-       };
+                       proto.populateForm(section, self);
 
-       this.firewall = {
-               getZoneColor: function(zone)
-               {
-                       if ($.isPlainObject(zone))
-                               zone = zone.name;
+                       if (!proto.virtual)
+                       {
+                               var br = section.taboption('physical', _luci2.cbi.CheckboxValue, 'type', {
+                                       caption:     _luci2.tr('Network bridge'),
+                                       description: _luci2.tr('Merges multiple devices into one logical bridge'),
+                                       optional:    true,
+                                       enabled:     'bridge',
+                                       disabled:    '',
+                                       initial:     ''
+                               });
 
-                       if (zone == 'lan')
-                               return '#90f090';
-                       else if (zone == 'wan')
-                               return '#f09090';
+                               section.taboption('physical', _luci2.cbi.DeviceList, '__iface_multi', {
+                                       caption:     _luci2.tr('Devices'),
+                                       multiple:    true,
+                                       bridges:     false
+                               }).depends('type', true);
+
+                               section.taboption('physical', _luci2.cbi.DeviceList, '__iface_single', {
+                                       caption:     _luci2.tr('Device'),
+                                       multiple:    false,
+                                       bridges:     true
+                               }).depends('type', false);
+
+                               var mac = section.taboption('physical', _luci2.cbi.InputValue, 'macaddr', {
+                                       caption:     _luci2.tr('Override MAC'),
+                                       optional:    true,
+                                       placeholder: device ? device.getMACAddress() : undefined,
+                                       datatype:    'macaddr'
+                               })
 
-                       for (var i = 0, hash = 0;
-                                i < zone.length;
-                                hash = zone.charCodeAt(i++) + ((hash << 5) - hash));
+                               mac.ucivalue = function(sid)
+                               {
+                                       if (device)
+                                               return device.get('macaddr');
 
-                       for (var i = 0, color = '#';
-                                i < 3;
-                                color += ('00' + ((hash >> i++ * 8) & 0xFF).tozoneing(16)).slice(-2));
+                                       return this.callSuper('ucivalue', sid);
+                               };
 
-                       return color;
-               },
+                               mac.save = function(sid)
+                               {
+                                       if (!this.changed(sid))
+                                               return false;
 
-               findZoneByNetwork: function(network)
-               {
-                       var self = this;
-                       var zone = undefined;
+                                       if (device)
+                                               device.set('macaddr', this.formvalue(sid));
+                                       else
+                                               this.callSuper('set', sid);
 
-                       return _luci2.uci.foreach('firewall', 'zone', function(z) {
-                               if (!z.name || !z.network)
-                                       return;
+                                       return true;
+                               };
+                       }
 
-                               if (!$.isArray(z.network))
-                                       z.network = z.network.split(/\s+/);
+                       section.taboption('physical', _luci2.cbi.InputValue, 'mtu', {
+                               caption:     _luci2.tr('Override MTU'),
+                               optional:    true,
+                               placeholder: device ? device.getMTU() : undefined,
+                               datatype:    'range(1, 9000)'
+                       });
 
-                               for (var i = 0; i < z.network.length; i++)
+                       section.taboption('physical', _luci2.cbi.InputValue, 'metric', {
+                               caption:     _luci2.tr('Override Metric'),
+                               optional:    true,
+                               placeholder: 0,
+                               datatype:    'uinteger'
+                       });
+
+                       for (var field in section.fields)
+                       {
+                               switch (field)
                                {
-                                       if (z.network[i] == network)
-                                       {
-                                               zone = z;
-                                               break;
-                                       }
+                               case 'proto':
+                                       break;
+
+                               case '_confirm':
+                                       for (var i = 0; i < protos.length; i++)
+                                               if (protos[i].name != (this.get('proto') || 'none'))
+                                                       section.fields[field].depends('proto', protos[i].name);
+                                       break;
+
+                               default:
+                                       section.fields[field].depends('proto', this.get('proto') || 'none', true);
+                                       break;
                                }
-                       }).then(function() {
-                               if (zone)
-                                       zone.color = self.getZoneColor(zone);
+                       }
 
-                               return zone;
-                       });
+                       return map;
                }
-       };
+       });
+
+       this.NetworkModel.Protocol = this.NetworkModel.Interface.extend({
+               description: '__unknown__',
+               tunnel:      false,
+               virtual:     false,
+
+               populateForm: function(section, iface)
+               {
+
+               }
+       });
 
        this.system = {
                getSystemInfo: _luci2.rpc.declare({
@@ -1626,6 +3154,41 @@ function LuCI2()
                                window.clearInterval(this._hearbeatInterval);
                                delete this._hearbeatInterval;
                        }
+               },
+
+
+               _acls: { },
+
+               _fetch_acls: _luci2.rpc.declare({
+                       object: 'session',
+                       method: 'access',
+                       expect: { '': { } }
+               }),
+
+               _fetch_acls_cb: function(acls)
+               {
+                       _luci2.session._acls = acls;
+               },
+
+               updateACLs: function()
+               {
+                       return _luci2.session._fetch_acls()
+                               .then(_luci2.session._fetch_acls_cb);
+               },
+
+               hasACL: function(scope, object, func)
+               {
+                       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;
                }
        };
 
@@ -1653,6 +3216,7 @@ function LuCI2()
 
                        var state = _luci2.ui._loading || (_luci2.ui._loading = {
                                modal: $('<div />')
+                                       .css('z-index', 2000)
                                        .addClass('modal fade')
                                        .append($('<div />')
                                                .addClass('modal-dialog')
@@ -1705,13 +3269,13 @@ function LuCI2()
                        {
                                state.dialog.modal('hide');
 
-                               return;
+                               return state.dialog;
                        }
 
                        var cnt = state.dialog.children().children().children('div.modal-body');
                        var ftr = state.dialog.children().children().children('div.modal-footer');
 
-                       ftr.empty();
+                       ftr.empty().show();
 
                        if (options.style == 'confirm')
                        {
@@ -1732,10 +3296,21 @@ function LuCI2()
                                        .attr('disabled', true));
                        }
 
+                       if (options.wide)
+                       {
+                               state.dialog.addClass('wide');
+                       }
+                       else
+                       {
+                               state.dialog.removeClass('wide');
+                       }
+
                        state.dialog.find('h4:first').text(title);
                        state.dialog.modal('show');
 
                        cnt.empty().append(content);
+
+                       return state.dialog;
                },
 
                upload: function(title, content, options)
@@ -2213,6 +3788,7 @@ function LuCI2()
                                                switch (c[0])
                                                {
                                                case 'order':
+                                                       log.push('uci reorder %s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2]));
                                                        break;
 
                                                case 'remove':
@@ -2920,7 +4496,7 @@ function LuCI2()
                                                                else if (typeof types[label] == 'function')
                                                                {
                                                                        stack.push(types[label]);
-                                                                       stack.push(null);
+                                                                       stack.push([ ]);
                                                                }
                                                                else
                                                                {
@@ -2939,7 +4515,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;
                                                }
@@ -3389,6 +4965,7 @@ function LuCI2()
                        }
 
                        i.error = $('<div />')
+                               .hide()
                                .addClass('label label-danger');
 
                        i.widget = $('<div />')
@@ -3525,7 +5102,7 @@ function LuCI2()
                                        d.elem.parents('div.form-group, td').first().addClass('luci2-form-error');
                                        d.elem.parents('div.input-group, div.form-group, td').first().addClass('has-error');
 
-                                       d.inst.error.text(_luci2.tr('Field must not be empty'));
+                                       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]))
@@ -3533,7 +5110,7 @@ function LuCI2()
                                        d.elem.parents('div.form-group, td').first().addClass('luci2-form-error');
                                        d.elem.parents('div.input-group, div.form-group, td').first().addClass('has-error');
 
-                                       d.inst.error.text(validation.message.format.apply(validation.message, vstack[1]));
+                                       d.inst.error.text(validation.message.format.apply(validation.message, vstack[1])).show();
                                        rv = false;
                                }
                                else
@@ -3544,7 +5121,7 @@ function LuCI2()
                                        if (d.multi && d.inst.widget && d.inst.widget.find('input.error, select.error').length > 0)
                                                rv = false;
                                        else
-                                               d.inst.error.text('');
+                                               d.inst.error.text('').hide();
                                }
                        }
 
@@ -3749,7 +5326,6 @@ function LuCI2()
                        if (typeof(o.disabled) == 'undefined') o.disabled = '0';
 
                        var i = $('<input />')
-                               .addClass('form-control')
                                .attr('id', this.id(sid))
                                .attr('type', 'checkbox')
                                .prop('checked', this.ucivalue(sid));
@@ -3902,16 +5478,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;
@@ -4334,61 +5907,41 @@ function LuCI2()
                        return $('<div />')
                                .addClass('form-control-static')
                                .attr('id', this.id(sid))
-                               .html(this.ucivalue(sid));
-               },
+                               .html(this.ucivalue(sid));
+               },
+
+               formvalue: function(sid)
+               {
+                       return this.ucivalue(sid);
+               }
+       });
+
+       this.cbi.ButtonValue = this.cbi.AbstractValue.extend({
+               widget: function(sid)
+               {
+                       this.options.optional = true;
+
+                       var btn = $('<button />')
+                               .addClass('btn btn-default')
+                               .attr('id', this.id(sid))
+                               .attr('type', 'button')
+                               .text(this.label('text'));
 
-               formvalue: function(sid)
-               {
-                       return this.ucivalue(sid);
+                       return this.validator(sid, btn);
                }
        });
 
        this.cbi.NetworkList = this.cbi.AbstractValue.extend({
                load: function(sid)
                {
-                       var self = this;
-
-                       if (!self.interfaces)
-                       {
-                               self.interfaces = [ ];
-                               return _luci2.network.getNetworkStatus().then(function(ifaces) {
-                                       self.interfaces = ifaces;
-                                       self = null;
-                               });
-                       }
-
-                       return undefined;
+                       return _luci2.NetworkModel.init();
                },
 
                _device_icon: function(dev)
                {
-                       var type = 'ethernet';
-                       var desc = _luci2.tr('Ethernet device');
-
-                       if (dev.type == 'IP tunnel')
-                       {
-                               type = 'tunnel';
-                               desc = _luci2.tr('Tunnel interface');
-                       }
-                       else if (dev['bridge-members'])
-                       {
-                               type = 'bridge';
-                               desc = _luci2.tr('Bridge');
-                       }
-                       else if (dev.wireless)
-                       {
-                               type = 'wifi';
-                               desc = _luci2.tr('Wireless Network');
-                       }
-                       else if (dev.device.indexOf('.') > 0)
-                       {
-                               type = 'vlan';
-                               desc = _luci2.tr('VLAN interface');
-                       }
-
                        return $('<img />')
-                               .attr('src', _luci2.globals.resource + '/icons/' + type + (dev.up ? '' : '_disabled') + '.png')
-                               .attr('title', '%s (%s)'.format(desc, dev.device));
+                               .attr('src', dev.icon())
+                               .attr('title', '%s (%s)'.format(dev.description(), dev.name() || '?'));
                },
 
                widget: function(sid)
@@ -4408,48 +5961,48 @@ function LuCI2()
                                for (var i = 0; i < value.length; i++)
                                        check[value[i]] = true;
 
-                       if (this.interfaces)
+                       var interfaces = _luci2.NetworkModel.getInterfaces();
+
+                       for (var i = 0; i < interfaces.length; i++)
                        {
-                               for (var i = 0; i < this.interfaces.length; i++)
-                               {
-                                       var iface = this.interfaces[i];
-                                       var badge = $('<span />')
-                                               .addClass('badge')
-                                               .text('%s: '.format(iface['interface']));
-
-                                       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
-                                               badge.append($('<em />').text(_luci2.tr('(No devices attached)')));
+                               var iface = interfaces[i];
+                               var badge = $('<span />')
+                                       .addClass('badge')
+                                       .text('%s: '.format(iface.name()));
+
+                               var dev = iface.getDevice();
+                               var subdevs = iface.getSubdevices();
+
+                               if (subdevs.length)
+                                       for (var j = 0; j < subdevs.length; j++)
+                                               badge.append(this._device_icon(subdevs[j]));
+                               else if (dev)
+                                       badge.append(this._device_icon(dev));
+                               else
+                                       badge.append($('<em />').text(_luci2.tr('(No devices attached)')));
 
-                                       $('<li />')
-                                               .append($('<label />')
-                                                       .addClass('radio inline')
-                                                       .append($('<input />')
-                                                               .attr('name', itype + id)
-                                                               .attr('type', itype)
-                                                               .attr('value', iface['interface'])
-                                                               .prop('checked', !!check[iface['interface']])
-                                                               .addClass('form-control'))
-                                                       .append(badge))
-                                               .appendTo(ul);
-                               }
+                               $('<li />')
+                                       .append($('<label />')
+                                               .addClass(itype + ' inline')
+                                               .append($('<input />')
+                                                       .attr('name', itype + id)
+                                                       .attr('type', itype)
+                                                       .attr('value', iface.name())
+                                                       .prop('checked', !!check[iface.name()]))
+                                               .append(badge))
+                                       .appendTo(ul);
                        }
 
                        if (!this.options.multiple)
                        {
                                $('<li />')
                                        .append($('<label />')
-                                               .addClass('radio inline text-muted')
+                                               .addClass(itype + ' inline text-muted')
                                                .append($('<input />')
                                                        .attr('name', itype + id)
                                                        .attr('type', itype)
                                                        .attr('value', '')
-                                                       .prop('checked', !value)
-                                                       .addClass('form-control'))
+                                                       .prop('checked', $.isEmptyObject(check)))
                                                .append(_luci2.tr('unspecified')))
                                        .appendTo(ul);
                        }
@@ -4614,19 +6167,21 @@ function LuCI2()
                                                inval++;
 
                                if (inval > 0)
-                                       stbadge.text(inval)
+                                       stbadge.show()
+                                               .text(inval)
                                                .attr('title', _luci2.trp('1 Error', '%d Errors', inval).format(inval));
                                else
-                                       stbadge.text('');
+                                       stbadge.hide();
 
                                invals += inval;
                        }
 
                        if (invals > 0)
-                               badge.text(invals)
+                               badge.show()
+                                       .text(invals)
                                        .attr('title', _luci2.trp('1 Error', '%d Errors', invals).format(invals));
                        else
-                               badge.text('');
+                               badge.hide();
 
                        return invals;
                },
@@ -4648,10 +6203,11 @@ function LuCI2()
                        var badge = $('#' + this.id('sectiontab')).children('span:first');
 
                        if (this.error_count > 0)
-                               badge.text(this.error_count)
+                               badge.show()
+                                       .text(this.error_count)
                                        .attr('title', _luci2.trp('1 Error', '%d Errors', this.error_count).format(this.error_count));
                        else
-                               badge.text('');
+                               badge.hide();
 
                        return (this.error_count == 0);
                }
@@ -4676,7 +6232,7 @@ function LuCI2()
 
                sections: function(cb)
                {
-                       var s1 = this.map.ucisections(this.map.uci_package);
+                       var s1 = _luci2.uci.sections(this.map.uci_package);
                        var s2 = [ ];
 
                        for (var i = 0; i < s1.length; i++)
@@ -4746,7 +6302,6 @@ function LuCI2()
                        var addb = text.next();
                        var errt = addb.next();
                        var name = text.val();
-                       var used = false;
 
                        if (!/^[a-zA-Z0-9_]*$/.test(name))
                        {
@@ -4756,21 +6311,7 @@ function LuCI2()
                                return false;
                        }
 
-                       for (var sid in self.map.uci.values[self.map.uci_package])
-                               if (sid == name)
-                               {
-                                       used = true;
-                                       break;
-                               }
-
-                       for (var sid in self.map.uci.creates[self.map.uci_package])
-                               if (sid == name)
-                               {
-                                       used = true;
-                                       break;
-                               }
-
-                       if (used)
+                       if (_luci2.uci.get(self.map.uci_package, name))
                        {
                                errt.text(_luci2.tr('Name already used')).show();
                                text.addClass('error');
@@ -4839,17 +6380,7 @@ function LuCI2()
 
                        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;
+                               _luci2.uci.swap(self.map.uci_package, s[cur_idx]['.name'], s[new_idx]['.name']);
 
                                self.map.save();
                                self.map.redraw();
@@ -5306,17 +6837,17 @@ function LuCI2()
                sections: function(cb)
                {
                        var sa = [ ];
-                       var pkg = this.map.uci.values[this.map.uci_package];
+                       var sl = _luci2.uci.sections(this.map.uci_package);
 
-                       for (var s in pkg)
-                               if (pkg[s]['.name'] == this.uci_type)
+                       for (var i = 0; i < sl.length; i++)
+                               if (sl[i]['.name'] == this.uci_type)
                                {
-                                       sa.push(pkg[s]);
+                                       sa.push(sl[i]);
                                        break;
                                }
 
                        if (typeof(cb) == 'function' && sa.length > 0)
-                               cb.apply(this, [ sa[0] ]);
+                               cb.call(this, sa[0]);
 
                        return sa;
                }
@@ -5341,22 +6872,12 @@ function LuCI2()
                        this.sections = [ ];
                        this.options = _luci2.defaults(options, {
                                save:    function() { },
-                               prepare: function() {
-                                       return _luci2.uci.writable(function(writable) {
-                                               self.options.readonly = !writable;
-                                       });
-                               }
+                               prepare: function() { }
                        });
                },
 
-               _load_cb: function(packages)
+               _load_cb: function()
                {
-                       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++)
@@ -5382,16 +6903,6 @@ function LuCI2()
                load: function()
                {
                        var self = this;
-
-                       this.uci = {
-                               newid:   0,
-                               values:  { },
-                               creates: { },
-                               changes: { },
-                               deletes: { },
-                               reorder: false
-                       };
-
                        var packages = { };
 
                        for (var i = 0; i < this.sections.length; i++)
@@ -5399,13 +6910,12 @@ function LuCI2()
 
                        packages[this.uci_package] = true;
 
-                       _luci2.rpc.batch();
-
                        for (var pkg in packages)
-                               _luci2.uci.get_all(pkg);
+                               if (!_luci2.uci.writable(pkg))
+                                       this.options.readonly = true;
 
-                       return _luci2.rpc.flush().then(function(packages) {
-                               return self._load_cb(packages);
+                       return _luci2.uci.load(_luci2.toArray(packages)).then(function() {
+                               return self._load_cb();
                        });
                },
 
@@ -5568,166 +7078,22 @@ function LuCI2()
 
                add: function(conf, type, name)
                {
-                       var c = this.uci.creates;
-                       var s = '.new.%d'.format(this.uci.newid++);
-
-                       if (!c[conf])
-                               c[conf] = { };
-
-                       c[conf][s] = {
-                               '.type':      type,
-                               '.name':      s,
-                               '.create':    name,
-                               '.anonymous': !name,
-                               '.index':     1000 + this.uci.newid
-                       };
-
-                       return s;
+                       return _luci2.uci.add(conf, type, name);
                },
 
                remove: function(conf, sid)
                {
-                       var n = this.uci.creates;
-                       var c = this.uci.changes;
-                       var d = this.uci.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];
-
-                               if (!d[conf])
-                                       d[conf] = { };
-
-                               d[conf][sid] = true;
-                       }
-               },
-
-               ucisections: function(conf, cb)
-               {
-                       var sa = [ ];
-                       var pkg = this.uci.values[conf];
-                       var crt = this.uci.creates[conf];
-                       var del = this.uci.deletes[conf];
-
-                       if (!pkg)
-                               return sa;
-
-                       for (var s in pkg)
-                               if (!del || del[s] !== true)
-                                       sa.push(pkg[s]);
-
-                       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] ]);
-
-                       return sa;
+                       return _luci2.uci.remove(conf, sid);
                },
 
                get: function(conf, sid, opt)
                {
-                       var v = this.uci.values;
-                       var n = this.uci.creates;
-                       var c = this.uci.changes;
-                       var d = this.uci.deletes;
-
-                       /* requested option in a just created section */
-                       if (sid.indexOf('.new.') == 0)
-                       {
-                               if (!n[conf])
-                                       return undefined;
-
-                               if (typeof(opt) == 'undefined')
-                                       return (n[conf][sid] || { });
-
-                               return n[conf][sid][opt];
-                       }
-
-                       /* 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;
-
-                                       for (var i = 0; i < d[conf][sid].length; i++)
-                                               if (d[conf][sid][i] == opt)
-                                                       return undefined;
-                               }
-
-                               /* check whether option was changed */
-                               if (c[conf] && c[conf][sid] && typeof(c[conf][sid][opt]) != 'undefined')
-                                       return c[conf][sid][opt];
-
-                               /* return base value */
-                               if (v[conf] && v[conf][sid])
-                                       return v[conf][sid][opt];
-
-                               return undefined;
-                       }
-
-                       /* requested an entire section */
-                       if (v[conf])
-                               return (v[conf][sid] || { });
-
-                       return undefined;
+                       return _luci2.uci.get(conf, sid, opt);
                },
 
                set: function(conf, sid, opt, val)
                {
-                       var n = this.uci.creates;
-                       var c = this.uci.changes;
-                       var d = this.uci.deletes;
-
-                       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')
-                       {
-                               if (!c[conf])
-                                       c[conf] = { };
-
-                               if (!c[conf][sid])
-                                       c[conf][sid] = { };
-
-                               c[conf][sid][opt] = val;
-                       }
-                       else
-                       {
-                               if (!d[conf])
-                                       d[conf] = { };
-
-                               if (!d[conf][sid])
-                                       d[conf][sid] = [ ];
-
-                               d[conf][sid].push(opt);
-                       }
+                       return _luci2.uci.set(conf, sid, opt, val);
                },
 
                validate: function()
@@ -5745,127 +7111,35 @@ function LuCI2()
 
                save: function()
                {
-                       if (this.options.readonly)
+                       var self = this;
+
+                       if (self.options.readonly)
                                return _luci2.deferrable();
 
-                       var deferreds = [ _luci2.deferrable(this.options.save()) ];
+                       var deferreds = [ ];
 
-                       for (var i = 0; i < this.sections.length; i++)
+                       for (var i = 0; i < self.sections.length; i++)
                        {
-                               if (this.sections[i].options.readonly)
+                               if (self.sections[i].options.readonly)
                                        continue;
 
-                               for (var f in this.sections[i].fields)
+                               for (var f in self.sections[i].fields)
                                {
-                                       if (typeof(this.sections[i].fields[f].save) != 'function')
+                                       if (typeof(self.sections[i].fields[f].save) != 'function')
                                                continue;
 
-                                       var s = this.sections[i].sections();
+                                       var s = self.sections[i].sections();
                                        for (var j = 0; j < s.length; j++)
                                        {
-                                               var rv = this.sections[i].fields[f].save(s[j]['.name']);
+                                               var rv = self.sections[i].fields[f].save(s[j]['.name']);
                                                if (_luci2.isDeferred(rv))
                                                        deferreds.push(rv);
                                        }
                                }
                        }
 
-                       return $.when.apply($, deferreds);
-               },
-
-               _send_uci_reorder: function()
-               {
-                       if (!this.uci.reorder)
-                               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 this.uci.values)
-                       {
-                               var o = [ ];
-
-                               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: { }
-                                               };
-
-                                               for (var k in this.uci.creates[c][s])
-                                               {
-                                                       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];
-                                               }
-
-                                               snew.push(this.uci.creates[c][s]);
-
-                                               _luci2.uci.add(r.config, r.type, r.name, r.values);
-                                       }
-
-                       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]);
-
-                       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);
-                                       }
-
-                       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();
+                       return $.when.apply($, deferreds).then(function() {
+                               return _luci2.deferrable(self.options.save());
                        });
                },
 
@@ -5880,7 +7154,7 @@ function LuCI2()
                        _luci2.ui.loading(true);
 
                        return this.save().then(function() {
-                               return self._send_uci();
+                               return _luci2.uci.save();
                        }).then(function() {
                                return _luci2.ui.updateChanges();
                        }).then(function() {