luci2: remove dead code from LuCI2.cbi.Map()
[project/luci2/ui.git] / luci2 / htdocs / luci2 / luci2.js
index e401c7c..ffa58ea 100644 (file)
@@ -387,7 +387,7 @@ function LuCI2()
        };
 
        this.globals = {
-               timeout:  3000,
+               timeout:  15000,
                resource: '/luci2',
                sid:      '00000000000000000000000000000000'
        };
@@ -584,13 +584,44 @@ function LuCI2()
 
                },
 
-               changes: _luci2.rpc.declare({
+               configs: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'configs',
+                       expect: { configs: [ ] }
+               }),
+
+               _changes: _luci2.rpc.declare({
                        object: 'uci',
                        method: 'changes',
                        params: [ 'config' ],
                        expect: { changes: [ ] }
                }),
 
+               changes: function(config)
+               {
+                       if (typeof(config) == 'string')
+                               return this._changes(config);
+
+                       var configlist;
+                       return this.configs().then(function(configs) {
+                               _luci2.rpc.batch();
+                               configlist = configs;
+
+                               for (var i = 0; i < configs.length; i++)
+                                       _luci2.uci._changes(configs[i]);
+
+                               return _luci2.rpc.flush();
+                       }).then(function(changes) {
+                               var rv = { };
+
+                               for (var i = 0; i < configlist.length; i++)
+                                       if (changes[i].length)
+                                               rv[configlist[i]] = changes[i];
+
+                               return rv;
+                       });
+               },
+
                commit: _luci2.rpc.declare({
                        object: 'uci',
                        method: 'commit',
@@ -757,13 +788,13 @@ function LuCI2()
                                        var net = nets[i] = networks[i];
                                        var dev = net.l3_device || net.l2_device;
                                        if (dev)
-                                               net.device = devs[dev] = { };
+                                               net.device = devs[dev] || (devs[dev] = { });
                                }
 
                                _luci2.rpc.batch();
 
                                for (var dev in devs)
-                                       _luci2.network.listDeviceNamestatus(dev);
+                                       _luci2.network.getDeviceStatus(dev);
 
                                return _luci2.rpc.flush();
                        }).then(function(devices) {
@@ -786,7 +817,7 @@ function LuCI2()
                                                if (!devs[brm[j]])
                                                {
                                                        devs[brm[j]] = { };
-                                                       _luci2.network.listDeviceNamestatus(brm[j]);
+                                                       _luci2.network.getDeviceStatus(brm[j]);
                                                }
 
                                                devs[devices[i]['device']].subdevices[j] = devs[brm[j]];
@@ -836,6 +867,9 @@ function LuCI2()
 
                                for (var i = 0; i < interfaces.length; i++)
                                {
+                                       if (!interfaces[i].route)
+                                               continue;
+
                                        for (var j = 0; j < interfaces[i].route.length; j++)
                                        {
                                                var rt = interfaces[i].route[j];
@@ -897,7 +931,7 @@ function LuCI2()
                        }
                }),
 
-               listDeviceNamestatus: _luci2.rpc.declare({
+               getDeviceStatus: _luci2.rpc.declare({
                        object: 'network.device',
                        method: 'status',
                        params: [ 'name' ],
@@ -912,6 +946,73 @@ function LuCI2()
                        object: 'luci2.network',
                        method: 'conntrack_count',
                        expect: { '': { count: 0, limit: 0 } }
+               }),
+
+               listSwitchNames: _luci2.rpc.declare({
+                       object: 'luci2.network',
+                       method: 'switch_list',
+                       expect: { switches: [ ] }
+               }),
+
+               getSwitchInfo: _luci2.rpc.declare({
+                       object: 'luci2.network',
+                       method: 'switch_info',
+                       params: [ 'switch' ],
+                       expect: { info: { } },
+                       filter: function(data, params) {
+                               data['attrs']      = data['switch'];
+                               data['vlan_attrs'] = data['vlan'];
+                               data['port_attrs'] = data['port'];
+                               data['switch']     = params['switch'];
+
+                               delete data.vlan;
+                               delete data.port;
+
+                               return data;
+                       }
+               }),
+
+               getSwitchStatus: _luci2.rpc.declare({
+                       object: 'luci2.network',
+                       method: 'switch_status',
+                       params: [ 'switch' ],
+                       expect: { ports: [ ] }
+               }),
+
+
+               runPing: _luci2.rpc.declare({
+                       object: 'luci2.network',
+                       method: 'ping',
+                       params: [ 'data' ],
+                       expect: { '': { code: -1 } }
+               }),
+
+               runPing6: _luci2.rpc.declare({
+                       object: 'luci2.network',
+                       method: 'ping6',
+                       params: [ 'data' ],
+                       expect: { '': { code: -1 } }
+               }),
+
+               runTraceroute: _luci2.rpc.declare({
+                       object: 'luci2.network',
+                       method: 'traceroute',
+                       params: [ 'data' ],
+                       expect: { '': { code: -1 } }
+               }),
+
+               runTraceroute6: _luci2.rpc.declare({
+                       object: 'luci2.network',
+                       method: 'traceroute6',
+                       params: [ 'data' ],
+                       expect: { '': { code: -1 } }
+               }),
+
+               runNslookup: _luci2.rpc.declare({
+                       object: 'luci2.network',
+                       method: 'nslookup',
+                       params: [ 'data' ],
+                       expect: { '': { code: -1 } }
                })
        };
 
@@ -1292,6 +1393,18 @@ function LuCI2()
                }),
 
 
+               testReset: _luci2.rpc.declare({
+                       object: 'luci2.system',
+                       method: 'reset_test',
+                       expect: { supported: false }
+               }),
+
+               startReset: _luci2.rpc.declare({
+                       object: 'luci2.system',
+                       method: 'reset_start'
+               }),
+
+
                performReboot: _luci2.rpc.declare({
                        object: 'luci2.system',
                        method: 'reboot'
@@ -1452,16 +1565,32 @@ function LuCI2()
 
        this.ui = {
 
+               saveScrollTop: function()
+               {
+                       this._scroll_top = $(document).scrollTop();
+               },
+
+               restoreScrollTop: function()
+               {
+                       if (typeof(this._scroll_top) == 'undefined')
+                               return;
+
+                       $(document).scrollTop(this._scroll_top);
+
+                       delete this._scroll_top;
+               },
+
                loading: function(enable)
                {
                        var win = $(window);
                        var body = $('body');
-                       var div = _luci2._modal || (
-                               _luci2._modal = $('<div />')
+
+                       var state = _luci2.ui._loading || (_luci2.ui._loading = {
+                               modal: $('<div />')
                                        .addClass('cbi-modal-loader')
                                        .append($('<div />').text(_luci2.tr('Loading data...')))
                                        .appendTo(body)
-                       );
+                       });
 
                        if (enable)
                        {
@@ -1469,13 +1598,13 @@ function LuCI2()
                                body.css('padding', 0);
                                body.css('width', win.width());
                                body.css('height', win.height());
-                               div.css('width', win.width());
-                               div.css('height', win.height());
-                               div.show();
+                               state.modal.css('width', win.width());
+                               state.modal.css('height', win.height());
+                               state.modal.show();
                        }
                        else
                        {
-                               div.hide();
+                               state.modal.hide();
                                body.css('overflow', '');
                                body.css('padding', '');
                                body.css('width', '');
@@ -1487,8 +1616,9 @@ function LuCI2()
                {
                        var win = $(window);
                        var body = $('body');
-                       var div = _luci2._dialog || (
-                               _luci2._dialog = $('<div />')
+
+                       var state = _luci2.ui._dialog || (_luci2.ui._dialog = {
+                               dialog: $('<div />')
                                        .addClass('cbi-modal-dialog')
                                        .append($('<div />')
                                                .append($('<div />')
@@ -1510,7 +1640,7 @@ function LuCI2()
                                                                        $(this).parent().parent().parent().hide();
                                                                }))))
                                        .appendTo(body)
-                       );
+                       });
 
                        if (typeof(options) != 'object')
                                options = { };
@@ -1523,13 +1653,13 @@ function LuCI2()
                                        .css('width', '')
                                        .css('height', '');
 
-                               _luci2._dialog.hide();
+                               state.dialog.hide();
 
                                return;
                        }
 
-                       var cnt = div.children().children('div.cbi-modal-dialog-body');
-                       var ftr = div.children().children('div.cbi-modal-dialog-footer');
+                       var cnt = state.dialog.children().children('div.cbi-modal-dialog-body');
+                       var ftr = state.dialog.children().children('div.cbi-modal-dialog-footer');
 
                        ftr.empty();
 
@@ -1560,29 +1690,29 @@ function LuCI2()
                                        .attr('disabled', true));
                        }
 
-                       div.find('div.cbi-modal-dialog-header').text(title);
-                       div.show();
+                       state.dialog.find('div.cbi-modal-dialog-header').text(title);
+                       state.dialog.show();
 
                        cnt
                                .css('max-height', Math.floor(win.height() * 0.70) + 'px')
                                .empty()
                                .append(content);
 
-                       div.children()
-                               .css('margin-top', -Math.floor(div.children().height() / 2) + 'px');
+                       state.dialog.children()
+                               .css('margin-top', -Math.floor(state.dialog.children().height() / 2) + 'px');
 
                        body.css('overflow', 'hidden');
                        body.css('padding', 0);
                        body.css('width', win.width());
                        body.css('height', win.height());
-                       div.css('width', win.width());
-                       div.css('height', win.height());
+                       state.dialog.css('width', win.width());
+                       state.dialog.css('height', win.height());
                },
 
                upload: function(title, content, options)
                {
-                       var form = _luci2._upload || (
-                               _luci2._upload = $('<form />')
+                       var state = _luci2.ui._upload || (_luci2.ui._upload = {
+                               form: $('<form />')
                                        .attr('method', 'post')
                                        .attr('action', '/cgi-bin/luci-upload')
                                        .attr('enctype', 'multipart/form-data')
@@ -1590,12 +1720,10 @@ function LuCI2()
                                        .append($('<p />'))
                                        .append($('<input />')
                                                .attr('type', 'hidden')
-                                               .attr('name', 'sessionid')
-                                               .attr('value', _luci2.globals.sid))
+                                               .attr('name', 'sessionid'))
                                        .append($('<input />')
                                                .attr('type', 'hidden')
-                                               .attr('name', 'filename')
-                                               .attr('value', options.filename))
+                                               .attr('name', 'filename'))
                                        .append($('<input />')
                                                .attr('type', 'file')
                                                .attr('name', 'filedata')
@@ -1610,11 +1738,9 @@ function LuCI2()
                                                .attr('name', 'cbi-fileupload-frame')
                                                .css('width', '1px')
                                                .css('height', '1px')
-                                               .css('visibility', 'hidden'))
-                       );
+                                               .css('visibility', 'hidden')),
 
-                       var finish = _luci2._upload_finish_cb || (
-                               _luci2._upload_finish_cb = function(ev) {
+                               finish_cb: function(ev) {
                                        $(this).off('load');
 
                                        var body = (this.contentDocument || this.contentWindow.document).body;
@@ -1639,41 +1765,43 @@ function LuCI2()
                                                        $('<p />').text(_luci2.tr('In case of network problems try uploading the file again.'))
                                                ], { style: 'close' });
                                        }
-                                       else if (typeof(ev.data.cb) == 'function')
+                                       else if (typeof(state.success_cb) == 'function')
                                        {
-                                               ev.data.cb(json);
+                                               state.success_cb(json);
                                        }
-                               }
-                       );
+                               },
 
-                       var confirm = _luci2._upload_confirm_cb || (
-                               _luci2._upload_confirm_cb = function() {
-                                       var d = _luci2._upload;
-                                       var f = d.find('.cbi-input-file');
-                                       var b = d.find('.progressbar');
-                                       var p = d.find('p');
+                               confirm_cb: function() {
+                                       var f = state.form.find('.cbi-input-file');
+                                       var b = state.form.find('.progressbar');
+                                       var p = state.form.find('p');
 
                                        if (!f.val())
                                                return;
 
-                                       d.find('iframe').on('load', { cb: options.success }, finish);
-                                       d.submit();
+                                       state.form.find('iframe').on('load', state.finish_cb);
+                                       state.form.submit();
 
                                        f.hide();
                                        b.show();
                                        p.text(_luci2.tr('File upload in progress …'));
 
-                                       _luci2._dialog.find('button').prop('disabled', true);
+                                       state.form.parent().parent().find('button').prop('disabled', true);
                                }
-                       );
+                       });
+
+                       state.form.find('.progressbar').hide();
+                       state.form.find('.cbi-input-file').val('').show();
+                       state.form.find('p').text(content || _luci2.tr('Select the file to upload and press "%s" to proceed.').format(_luci2.tr('Ok')));
 
-                       _luci2._upload.find('.progressbar').hide();
-                       _luci2._upload.find('.cbi-input-file').val('').show();
-                       _luci2._upload.find('p').text(content || _luci2.tr('Select the file to upload and press "%s" to proceed.').format(_luci2.tr('Ok')));
+                       state.form.find('[name=sessionid]').val(_luci2.globals.sid);
+                       state.form.find('[name=filename]').val(options.filename);
 
-                       _luci2.ui.dialog(title || _luci2.tr('File upload'), _luci2._upload, {
+                       state.success_cb = options.success;
+
+                       _luci2.ui.dialog(title || _luci2.tr('File upload'), state.form, {
                                style: 'confirm',
-                               confirm: confirm
+                               confirm: state.confirm_cb
                        });
                },
 
@@ -1751,32 +1879,10 @@ function LuCI2()
 
                login: function(invalid)
                {
-                       if (!_luci2._login_deferred || _luci2._login_deferred.state() != 'pending')
-                               _luci2._login_deferred = $.Deferred();
-
-                       /* try to find sid from hash */
-                       var sid = _luci2.getHash('id');
-                       if (sid && sid.match(/^[a-f0-9]{32}$/))
-                       {
-                               _luci2.globals.sid = sid;
-                               _luci2.session.isAlive().then(function(access) {
-                                       if (access)
-                                       {
-                                               _luci2.session.startHeartbeat();
-                                               _luci2._login_deferred.resolve();
-                                       }
-                                       else
-                                       {
-                                               _luci2.setHash('id', undefined);
-                                               _luci2.ui.login();
-                                       }
-                               });
-
-                               return _luci2._login_deferred;
-                       }
-
-                       var form = _luci2._login || (
-                               _luci2._login = $('<div />')
+                       var state = _luci2.ui._login || (_luci2.ui._login = {
+                               form: $('<form />')
+                                       .attr('target', '')
+                                       .attr('method', 'post')
                                        .append($('<p />')
                                                .addClass('alert-message')
                                                .text(_luci2.tr('Wrong username or password given!')))
@@ -1788,7 +1894,11 @@ function LuCI2()
                                                                .attr('type', 'text')
                                                                .attr('name', 'username')
                                                                .attr('value', 'root')
-                                                               .addClass('cbi-input-text'))))
+                                                               .addClass('cbi-input-text')
+                                                               .keypress(function(ev) {
+                                                                       if (ev.which == 10 || ev.which == 13)
+                                                                               state.confirm_cb();
+                                                               }))))
                                        .append($('<p />')
                                                .append($('<label />')
                                                        .text(_luci2.tr('Password'))
@@ -1796,13 +1906,15 @@ function LuCI2()
                                                        .append($('<input />')
                                                                .attr('type', 'password')
                                                                .attr('name', 'password')
-                                                               .addClass('cbi-input-password'))))
+                                                               .addClass('cbi-input-password')
+                                                               .keypress(function(ev) {
+                                                                       if (ev.which == 10 || ev.which == 13)
+                                                                               state.confirm_cb();
+                                                               }))))
                                        .append($('<p />')
-                                               .text(_luci2.tr('Enter your username and password above, then click "%s" to proceed.').format(_luci2.tr('Ok'))))
-                       );
+                                               .text(_luci2.tr('Enter your username and password above, then click "%s" to proceed.').format(_luci2.tr('Ok')))),
 
-                       var response_cb = _luci2._login_response_cb || (
-                               _luci2._login_response_cb = function(response) {
+                               response_cb: function(response) {
                                        if (!response.ubus_rpc_session)
                                        {
                                                _luci2.ui.login(true);
@@ -1813,16 +1925,13 @@ function LuCI2()
                                                _luci2.setHash('id', _luci2.globals.sid);
                                                _luci2.session.startHeartbeat();
                                                _luci2.ui.dialog(false);
-                                               _luci2._login_deferred.resolve();
+                                               state.deferred.resolve();
                                        }
-                               }
-                       );
+                               },
 
-                       var confirm_cb = _luci2._login_confirm_cb || (
-                               _luci2._login_confirm_cb = function() {
-                                       var d = _luci2._login;
-                                       var u = d.find('[name=username]').val();
-                                       var p = d.find('[name=password]').val();
+                               confirm_cb: function() {
+                                       var u = state.form.find('[name=username]').val();
+                                       var p = state.form.find('[name=password]').val();
 
                                        if (!u)
                                                return;
@@ -1840,21 +1949,47 @@ function LuCI2()
                                        );
 
                                        _luci2.globals.sid = '00000000000000000000000000000000';
-                                       _luci2.session.login(u, p).then(response_cb);
+                                       _luci2.session.login(u, p).then(state.response_cb);
                                }
-                       );
+                       });
+
+                       if (!state.deferred || state.deferred.state() != 'pending')
+                               state.deferred = $.Deferred();
+
+                       /* try to find sid from hash */
+                       var sid = _luci2.getHash('id');
+                       if (sid && sid.match(/^[a-f0-9]{32}$/))
+                       {
+                               _luci2.globals.sid = sid;
+                               _luci2.session.isAlive().then(function(access) {
+                                       if (access)
+                                       {
+                                               _luci2.session.startHeartbeat();
+                                               state.deferred.resolve();
+                                       }
+                                       else
+                                       {
+                                               _luci2.setHash('id', undefined);
+                                               _luci2.ui.login();
+                                       }
+                               });
+
+                               return state.deferred;
+                       }
 
                        if (invalid)
-                               form.find('.alert-message').show();
+                               state.form.find('.alert-message').show();
                        else
-                               form.find('.alert-message').hide();
+                               state.form.find('.alert-message').hide();
 
-                       _luci2.ui.dialog(_luci2.tr('Authorization Required'), form, {
+                       _luci2.ui.dialog(_luci2.tr('Authorization Required'), state.form, {
                                style: 'confirm',
-                               confirm: confirm_cb
+                               confirm: state.confirm_cb
                        });
 
-                       return _luci2._login_deferred;
+                       state.form.find('[name=password]').focus();
+
+                       return state.deferred;
                },
 
                cryptPassword: _luci2.rpc.declare({
@@ -1969,6 +2104,9 @@ function LuCI2()
                {
                        var name = node.view.split(/\//).join('.');
 
+                       if (_luci2.globals.currentView)
+                               _luci2.globals.currentView.finish();
+
                        _luci2.ui.renderViewMenu();
 
                        if (!_luci2._views)
@@ -1977,34 +2115,136 @@ function LuCI2()
                        _luci2.setHash('view', node.view);
 
                        if (_luci2._views[name] instanceof _luci2.ui.view)
+                       {
+                               _luci2.globals.currentView = _luci2._views[name];
                                return _luci2._views[name].render();
+                       }
 
-                       return $.ajax(_luci2.globals.resource + '/view/' + name + '.js', {
+                       var url = _luci2.globals.resource + '/view/' + name + '.js';
+
+                       return $.ajax(url, {
                                method: 'GET',
                                cache: true,
                                dataType: 'text'
                        }).then(function(data) {
                                try {
-                                       var viewConstructor = (new Function(['L', '$'], 'return ' + data))(_luci2, $);
+                                       var viewConstructorSource = (
+                                               '(function(L, $) { ' +
+                                                       'return %s' +
+                                               '})(_luci2, $);\n\n' +
+                                               '//@ sourceURL=%s'
+                                       ).format(data, url);
+
+                                       var viewConstructor = eval(viewConstructorSource);
 
                                        _luci2._views[name] = new viewConstructor({
                                                name: name,
                                                acls: node.write || { }
                                        });
 
+                                       _luci2.globals.currentView = _luci2._views[name];
                                        return _luci2._views[name].render();
                                }
-                               catch(e) { };
+                               catch(e) {
+                                       alert('Unable to instantiate view "%s": %s'.format(url, e));
+                               };
 
                                return $.Deferred().resolve();
                        });
                },
 
+               updateHostname: function()
+               {
+                       return _luci2.system.getBoardInfo().then(function(info) {
+                               if (info.hostname)
+                                       $('#hostname').text(info.hostname);
+                       });
+               },
+
+               updateChanges: function()
+               {
+                       return _luci2.uci.changes().then(function(changes) {
+                               var n = 0;
+                               var html = '';
+
+                               for (var config in changes)
+                               {
+                                       var log = [ ];
+
+                                       for (var i = 0; i < changes[config].length; i++)
+                                       {
+                                               var c = changes[config][i];
+
+                                               switch (c[0])
+                                               {
+                                               case 'order':
+                                                       break;
+
+                                               case 'remove':
+                                                       if (c.length < 3)
+                                                               log.push('uci delete %s.<del>%s</del>'.format(config, c[1]));
+                                                       else
+                                                               log.push('uci delete %s.%s.<del>%s</del>'.format(config, c[1], c[2]));
+                                                       break;
+
+                                               case 'rename':
+                                                       if (c.length < 4)
+                                                               log.push('uci rename %s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3]));
+                                                       else
+                                                               log.push('uci rename %s.%s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3], c[4]));
+                                                       break;
+
+                                               case 'add':
+                                                       log.push('uci add %s <ins>%s</ins> (= <ins><strong>%s</strong></ins>)'.format(config, c[2], c[1]));
+                                                       break;
+
+                                               case 'list-add':
+                                                       log.push('uci add_list %s.%s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3], c[4]));
+                                                       break;
+
+                                               case 'list-del':
+                                                       log.push('uci del_list %s.%s.<del>%s=<strong>%s</strong></del>'.format(config, c[1], c[2], c[3], c[4]));
+                                                       break;
+
+                                               case 'set':
+                                                       if (c.length < 4)
+                                                               log.push('uci set %s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2]));
+                                                       else
+                                                               log.push('uci set %s.%s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3], c[4]));
+                                                       break;
+                                               }
+                                       }
+
+                                       html += '<code>/etc/config/%s</code><pre class="uci-changes">%s</pre>'.format(config, log.join('\n'));
+                                       n += changes[config].length;
+                               }
+
+                               if (n > 0)
+                                       $('#changes')
+                                               .empty()
+                                               .show()
+                                               .append($('<a />')
+                                                       .attr('href', '#')
+                                                       .addClass('label')
+                                                       .addClass('notice')
+                                                       .text(_luci2.trcp('Pending configuration changes', '1 change', '%d changes', n).format(n))
+                                                       .click(function(ev) {
+                                                               _luci2.ui.dialog(_luci2.tr('Staged configuration changes'), html, { style: 'close' });
+                                                               ev.preventDefault();
+                                                       }));
+                               else
+                                       $('#changes')
+                                               .hide();
+                       });
+               },
+
                init: function()
                {
                        _luci2.ui.loading(true);
 
                        $.when(
+                               _luci2.ui.updateHostname(),
+                               _luci2.ui.updateChanges(),
                                _luci2.ui.renderMainMenu()
                        ).then(function() {
                                _luci2.ui.renderView(_luci2.globals.defaultNode).then(function() {
@@ -2083,6 +2323,42 @@ function LuCI2()
                        return this._fetch_template().then(function() {
                                return _luci2.deferrable(self.execute());
                        });
+               },
+
+               repeat: function(func, interval)
+               {
+                       var self = this;
+
+                       if (!self._timeouts)
+                               self._timeouts = [ ];
+
+                       var index = self._timeouts.length;
+
+                       if (typeof(interval) != 'number')
+                               interval = 5000;
+
+                       var setTimer, runTimer;
+
+                       setTimer = function() {
+                               self._timeouts[index] = window.setTimeout(runTimer, interval);
+                       };
+
+                       runTimer = function() {
+                               _luci2.deferrable(func.call(self)).then(setTimer);
+                       };
+
+                       runTimer();
+               },
+
+               finish: function()
+               {
+                       if ($.isArray(this._timeouts))
+                       {
+                               for (var i = 0; i < this._timeouts.length; i++)
+                                       window.clearTimeout(this._timeouts[i]);
+
+                               delete this._timeouts;
+                       }
                }
        });
 
@@ -2136,7 +2412,10 @@ function LuCI2()
                                var child = this.firstChildView(nodes[i]);
                                if (child)
                                {
-                                       $.extend(node, child);
+                                       for (var key in child)
+                                               if (!node.hasOwnProperty(key) && child.hasOwnProperty(key))
+                                                       node[key] = child[key];
+
                                        return node;
                                }
                        }
@@ -2421,7 +2700,9 @@ function LuCI2()
        this.ui.devicebadge = AbstractWidget.extend({
                render: function()
                {
-                       var dev = this.options.l3_device || this.options.device || '?';
+                       var l2dev = this.options.l2_device || this.options.device;
+                       var l3dev = this.options.l3_device;
+                       var dev = l3dev || l2dev || '?';
 
                        var span = document.createElement('span');
                                span.className = 'ifacebadge';
@@ -2462,7 +2743,7 @@ function LuCI2()
                                var type = 'ethernet';
                                var desc = _luci2.tr('Ethernet device');
 
-                               if (this.options.l3_device != this.options.device)
+                               if (l3dev != l2dev)
                                {
                                        type = 'tunnel';
                                        desc = _luci2.tr('Tunnel interface');
@@ -2712,6 +2993,7 @@ function LuCI2()
                                }
                        }
 
+                       validation.i18n('Must be a valid IPv6 address');
                        return false;
                },
 
@@ -3415,12 +3697,10 @@ function LuCI2()
 
                        if (chg)
                        {
-                               val = val ? this.options.enabled : this.options.disabled;
-
                                if (this.options.optional && val == this.options.initial)
                                        this.map.set(uci.config, uci.section, uci.option, undefined);
                                else
-                                       this.map.set(uci.config, uci.section, uci.option, val);
+                                       this.map.set(uci.config, uci.section, uci.option, val ? this.options.enabled : this.options.disabled);
                        }
 
                        return chg;
@@ -4312,10 +4592,14 @@ function LuCI2()
                        if (addb.prop('disabled') || name === '')
                                return;
 
+                       _luci2.ui.saveScrollTop();
+
                        self.active_panel = -1;
                        self.map.save();
                        self.add(name);
                        self.map.redraw();
+
+                       _luci2.ui.restoreScrollTop();
                },
 
                _remove: function(ev)
@@ -4323,10 +4607,17 @@ function LuCI2()
                        var self = ev.data.self;
                        var sid  = ev.data.sid;
 
+                       if (ev.data.index == (self.sections().length - 1))
+                               self.active_panel = -1;
+
+                       _luci2.ui.saveScrollTop();
+
                        self.map.save();
                        self.remove(sid);
                        self.map.redraw();
 
+                       _luci2.ui.restoreScrollTop();
+
                        ev.stopPropagation();
                },
 
@@ -4469,7 +4760,7 @@ function LuCI2()
                        return add;
                },
 
-               _render_remove: function(sid)
+               _render_remove: function(sid, index)
                {
                        var text = _luci2.tr('Remove');
                        var ttip = _luci2.tr('Remove this section');
@@ -4484,7 +4775,7 @@ function LuCI2()
                                .addClass('cbi-button')
                                .addClass('cbi-button-remove')
                                .val(text).attr('title', ttip)
-                               .click({ self: this, sid: sid }, this._remove);
+                               .click({ self: this, sid: sid, index: index }, this._remove);
                },
 
                _render_caption: function(sid)
@@ -4568,7 +4859,7 @@ function LuCI2()
                                        $('<div />')
                                                .addClass('cbi-section-remove')
                                                .addClass('right')
-                                               .append(this._render_remove(sid))
+                                               .append(this._render_remove(sid, panel_index))
                                                .appendTo(head);
 
                                var body = $('<div />')
@@ -4911,7 +5202,8 @@ function LuCI2()
                                deletes: { }
                        };
 
-                       this.active_panel = 0;
+                       if (typeof(this.active_panel) == 'undefined')
+                               this.active_panel = 0;
 
                        var packages = { };
 
@@ -5330,11 +5622,14 @@ function LuCI2()
                                                        _luci2.uci['delete'](c, s, (o === true) ? undefined : o);
                                                }
 
-                               return _luci2.rpc.flush();
+                               return _luci2.rpc.flush().then(function() {
+                                       return _luci2.ui.updateChanges();
+                               });
                        }, this));
 
                        var self = this;
 
+                       _luci2.ui.saveScrollTop();
                        _luci2.ui.loading(true);
 
                        return this.save().then(send_cb).then(function() {
@@ -5344,33 +5639,7 @@ function LuCI2()
                                self = null;
 
                                _luci2.ui.loading(false);
-                       });
-               },
-
-               dialog: function(id)
-               {
-                       var d = $('<div />');
-                       var p = $('<p />');
-
-                       $('<img />')
-                               .attr('src', _luci2.globals.resource + '/icons/loading.gif')
-                               .css('vertical-align', 'middle')
-                               .css('padding-right', '10px')
-                               .appendTo(p);
-
-                       p.append(_luci2.tr('Loading data...'));
-
-                       p.appendTo(d);
-                       d.appendTo(id);
-
-                       return d.dialog({
-                               modal: true,
-                               draggable: false,
-                               resizable: false,
-                               height: 90,
-                               open: function() {
-                                       $(this).parent().children('.ui-dialog-titlebar').hide();
-                               }
+                               _luci2.ui.restoreScrollTop();
                        });
                },