// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later

define([
    'jquery',
    '/common/toolbar.js',
    '/common/pad-types.js',
    '/components/nthen/index.js',
    '/common/sframe-common.js',
    '/common/common-interface.js',
    '/common/common-ui-elements.js',
    '/common/common-util.js',
    '/common/common-hash.js',
    '/common/inner/sidebar-layout.js',
    '/customize/messages.js',
    '/common/common-signing-keys.js',
    '/common/hyperscript.js',
    '/common/clipboard.js',
    'json.sortify',
    '/api/config',
    '/api/instance',
    '/lib/datepicker/flatpickr.js',
    '/install/onboardscreen.js',

    'css!/lib/datepicker/flatpickr.min.css',
    'css!/components/bootstrap/dist/css/bootstrap.min.css',
    'css!/components/components-font-awesome/css/font-awesome.min.css',
    'less!/admin/app-admin.less',
], function(
    $,
    Toolbar,
    PadTypes,
    nThen,
    SFCommon,
    UI,
    UIElements,
    Util,
    Hash,
    Sidebar,
    Messages,
    Keys,
    h,
    Clipboard,
    Sortify,
    ApiConfig,
    Instance,
    Flatpickr,
    Onboarding,
) {

    var APP = window.APP = {};

    var Nacl = window.nacl;
    var common;
    var sFrameChan;

    var andThen = function (common, $container) {
        const sidebar = Sidebar.create(common, 'admin', $container);
        // UNUSED-TRANSLATIONS:START
        var categories = {
            'general': { // Msg.admin_cat_general
                icon: 'fa fa-user-o',
                content: [
                    'instance-info-notice',
                    'name',
                    'description',
                    'email',
                    'jurisdiction',
                    'flush-cache',
                ]
            },
            'customize': { // Msg.admin_cat_customize
                icon: 'fa fa-paint-brush',
                content: [
                    'logo',
                    'color',
                ]
            },
            'admins': {
                icon: 'fa fa-users',
                content: [
                    'list-admins',
                    'add-admins'
                ]
            },
            'broadcast' : { // Msg.admin_cat_broadcast
                icon: 'fa fa-bullhorn',
                content : [
                    'notice',
                    'maintenance',
                    'survey',
                    'broadcast'
                ]
            },
            'security': { // Msg.admin_cat_security
                icon: 'fa fa-lock',
                content: [
                    'enableembeds',
                    'forcemfa',
                ]
            },
            'apps': { // Msg.admin_cat_apps
                icon: 'fa fa-wrench',
                content: [
                    'apps',
                ]
            },
            'users' : { // Msg.admin_cat_users
                icon : 'fa fa-address-card-o',
                content : [
                'registration',
                'invitation',
                'users'
                ]
            },
            'quota': { // Msg.admin_cat_quota
                icon: 'fa fa-hdd-o',
                content: [
                    'defaultlimit',
                    'setlimit',
                    'getlimits',
                ]
            },
            'database' : { // Msg.admin_cat_database
                icon : 'fa fa-database',
                content : [
                    'account-metadata',
                    'document-metadata',
                    'documents-deletion',
                    'block-metadata',
                    'totp-recovery',
                ]
            },
            'support' : { // Msg.admin_cat_support
                icon : 'fa fa-ambulance',
                content : [
                    'support-setup',
                    'support-team',
                ]
            },
            'stats' : { // Msg.admin_cat_stats
                icon : 'fa fa-line-chart',
                content : [
                    'refresh-stats',
                    'uptime',
                    'active-sessions',
                    'active-pads',
                    'open-files',
                    'registered',
                    'disk-usage',
                ]
            },
            'performance' : { // Msg.admin_cat_performance
                icon : 'fa fa-heartbeat',
                content : [
                    'refresh-performance',
                    'performance-profiling',
                    'enable-disk-measurements',
                    'bytes-written',
                ]
            },
            'network' : { // Msg.admin_cat_network
                icon : 'fa fa-sitemap',
                content : [
                    'update-available',
                    'checkup',
                    'block-daily-check',
                    'provide-aggregate-statistics',
                    'list-my-instance',

                    'consent-to-contact',
                    'remove-donate-button',
                    'instance-purpose',
                ]
            }
        };

        const blocks = sidebar.blocks;

        // EXTENSION_POINT:ADMIN_CATEGORY
        common.getExtensionsSync('ADMIN_CATEGORY').forEach(ext => {
            if (!ext || !ext.id || !ext.name || !ext.content) {
                return console.error('Invalid extension point', 'ADMIN_CATEGORY', ext);
            }
            if (categories[ext.id]) {
                return console.error('Extension point ID already used', ext);
            }
            console.error(ext);
            categories[ext.id] = {
                icon: ext.icon,
                name: ext.name,
                content: ext.content
            };
        });


        const flushCache = (cb) => {
            cb = cb || function () {};
            sFrameChan.query('Q_ADMIN_RPC', {
                cmd: 'FLUSH_CACHE',
            }, cb);
        };
        /*
        var flushCacheNotice = function () {
            var notice = UIElements.setHTML(h('p'), Messages.admin_reviewCheckupNotice);
            $(notice).find('a').attr({
                href: new URL('/checkup/', ApiConfig.httpUnsafeOrigin).href,
            }).click(function (ev) {
                ev.preventDefault();
                ev.stopPropagation();
                common.openURL('/checkup/');
            });
            var content = h('span', [
                UIElements.setHTML(h('p'), Messages.admin_cacheEvictionRequired),
                notice,
            ]);
            UI.alert(content);
        };
        */

        //general blocks

        // Msg.admin_flushCacheHint, .admin_flushCacheTitle, .admin_flushCacheButton
        sidebar.addItem('flush-cache', function (cb) {
            var button = blocks.activeButton('primary', '',
                    Messages.admin_flushCacheButton, done => {
                flushCache(function (e, data) {
                    done(!!data);
                    UI.alert(data ? Messages.admin_flushCacheDone || 'done' : 'error' + e);
                });
            });
            cb(button);
        });

        const evRefreshAdmins = Util.mkEvent();
        sidebar.addItem('list-admins', cb => {
            const removeAdmin = (edPublic, _cb) => {
                const cb = Util.mkAsync(_cb);
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'ADMIN_DECREE',
                    data: ['RM_ADMIN_KEY', [edPublic]]
                }, function (e, response) {
                    if (e || response.error) {
                        UI.warn(Messages.error);
                        console.error(e, response);
                        return void cb('ERROR');
                    }
                    if (typeof(cb) === "function") { cb(); }
                });
            };

            const header = [
                Messages.admin_listName,
                Messages.admin_listKey,
                Messages.admin_listAction
            ];
            var list = blocks.table(header, []);
            list.setAttribute('id', 'cp-admin-table');
            let div = blocks.block([list]);
            div.setAttribute('id', 'cp-admin-table-container');

            const refreshTable = () => {
                const admins = APP.instanceStatus.admins || [];
                const newRows = admins.map(obj => {
                    let { name, edPublic, hardcoded } = obj;
                    name = name || Messages.admin_admin;
                    let button = blocks.button('danger','fa-ban', Messages.admin_usersRemove);
                    let $b = $(button);
                    Util.onClickEnter($b, () => {
                        $b.prop('disabled', 'disabled');
                        UI.confirm(Messages.admin_listConfirm, yes => {
                            if (!yes) { return $b.prop('disabled', false); }
                            removeAdmin(edPublic, err => {
                                $b.prop('disabled', false);
                                if (err) { return; }
                                APP.updateStatus(function () {
                                    flushCache();
                                    evRefreshAdmins.fire();
                                });
                            });
                        });
                    });
                    let note = Messages.admin_listHardcoded;
                    let action = hardcoded ? note : button;

                    return [name, edPublic, action];
                });
                list.updateContent(newRows);
            };
            refreshTable();
            evRefreshAdmins.reg(() => {
                refreshTable();
            });

            cb(div);
        });
        sidebar.addItem('add-admins', cb => {
            const content = blocks.block();
            const metadataMgr = common.getMetadataMgr();
            const privateData = metadataMgr.getPrivateData();
            const $div = $(content);

            const addAdmin = (data, _cb) => {
                const cb = Util.mkAsync(_cb);
                const { ed, name } = data;
                const key = Hash.getPublicSigningKeyString(privateData.origin, name, ed);
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'ADMIN_DECREE',
                    data: ['ADD_ADMIN_KEY', [key]]
                }, function (e, response) {
                    if (e || response.error) {
                        UI.warn(Messages.error);
                        console.error(e, response);
                        return void cb('ERROR');
                    }
                    if (typeof(cb) === "function") { cb(); }
                    flushCache();
                });
            };

            const keyInput = blocks.input({
                placeholder: Messages.admin_accountMetadataPlaceholder
            });
            const keyLabel = blocks.labelledInput(Messages.admin_addKeyLabel, keyInput);
            const keyButton = blocks.button('primary', 'fa-plus', Messages.tag_add);
            const keyForm = blocks.form([keyLabel], blocks.nav([keyButton]));
            const $keyInput = $(keyInput).on('input', () => {
                let val = $keyInput.val().trim();
                if (!val) {
                    keyInput.setCustomValidity('');
                    return;
                }
                let key = Keys.canonicalize(val);

                if (keyInput.setCustomValidity) {
                    if (!key) {
                        const msg = Messages.admin_invalKey;
                        keyInput.setCustomValidity(msg);
                    } else {
                        keyInput.setCustomValidity('');
                    }
                }
            });

            const drawContacts = () => {
                $div.empty();
                const members = {};
                const admins = APP.instanceStatus.admins || [];
                admins.forEach(obj => {
                    const { edPublic, name, hardcoded, first } = obj;
                    members[edPublic] = { name, hardcoded, first };
                });

                // Remove admins from contacts list
                const friends = Util.clone(common.getFriends(false));
                Object.keys(friends).forEach((curve) => {
                    const ed = friends[curve]?.edPublic;
                    if (members[ed]) { delete friends[curve]; }
                });
                let contactsGrid = UIElements.getUserGrid(Messages.admin_addAdminsAdd, {
                    common: common,
                    list: true,
                    large: true,
                    data: friends
                }, function () {});
                let addBtn = blocks.button('primary', 'fa-plus', Messages.tag_add);
                Util.onClickEnter($(addBtn), () => {
                    var $sel = $(contactsGrid.div).find('.cp-usergrid-user.cp-selected');
                    if (!$sel.length) {
                        return UI.warn(Messages.admin_errorAddAdmins);
                    }
                    nThen((waitFor) => {
                        $sel.each((i, el) => {
                            const $el = $(el);
                            let ed = $el.attr('data-ed');
                            let name = $el.attr('data-name');
                            if (!ed || !name) {
                                console.error('Missing data on selected user', el);
                                return void UI.warn(Messages.error);
                            }
                            addAdmin({ed, name}, waitFor());
                        });
                    }).nThen(() => {
                        APP.updateStatus(function () {
                            evRefreshAdmins.fire();
                        });
                    });
                });
                evRefreshAdmins.reg(() => {
                    drawContacts();
                });

                const $keyBtn = $(keyButton);
                Util.onClickEnter($keyBtn, () => {
                    let val = $keyInput.val().trim();
                    let key = Keys.canonicalize(val);
                    if (!key) {
                        return UI.warn(Messages.admin_errorAddKeyLabel);
                    }
                    // We have a valid key
                    let name = Messages.admin_admin;
                    try {
                        let parsed = Keys.parseUser(val);
                        name = parsed.user;
                    } catch (e) {}
                    $keyBtn.prop('disabled', 'disabled');
                    addAdmin({ ed:key, name }, (err) => {
                        $keyBtn.prop('disabled', false);
                        if (!err) { $keyInput.val(''); }
                        // refresh
                        APP.updateStatus(function () {
                            evRefreshAdmins.fire();
                        });
                    });
                });

                const list = blocks.form([
                    //currentList.div,
                    contactsGrid.div,
                ], blocks.nav([addBtn]));
                $div.append([keyForm, list]);
            };
            drawContacts();
            cb(content);
        });

        var isHex = s => !/[^0-9a-f]/.test(s);

        var sframeCommand = function (command, data, cb) {
            sFrameChan.query('Q_ADMIN_RPC', {
                cmd: command,
                data: data,
            }, function (err, response) {
                if (err) { return void cb(err); }
                if (response && response.error) { return void cb(response.error); }
                try {
                    cb(void 0, response);
                } catch (err2) {
                    console.error(err2);
                }
            });
        };

        // XXX make this use blocks
        var makeMetadataTable = function (cls) {
            var table = h(`table.${cls || 'cp-account-stats'}`);
            var row = (label, value) => {
                table.appendChild(h('tr', [
                    h('td', h('strong', label)),
                    h('td', value)
                ]));
            };

            return {
                row: row,
                table: table,
            };
        };

        var getPrettySize = UIElements.prettySize;

        var localizeState = state => {
            var o = {
                'true': Messages.ui_true,
                'false': Messages.ui_false,
                'undefined': Messages.ui_undefined,
            };
            return o[state] || Messages.error;
        };

        var disable = $el => $el.attr('disabled', 'disabled');
        var enable = $el => $el.removeAttr('disabled');

        var maybeDate = function (d) {
            return d? new Date(d): Messages.ui_undefined;
        };

        var justifyDialog = (message, suggestion, implicit, explicit) => {
            UI.prompt(message, suggestion, result => {
                if (result === null) { return; }
                if (typeof(result) !== 'string') { result = ''; }
                else { result = result.trim(); }
                implicit(result); // remember the justification for next time
                explicit(result); // follow up with the action
            }, {
                ok: Messages.ui_confirm,
                inputOpts: {
                    placeholder: Messages.admin_archiveNote || '',
                },
            });
        };

        var archiveReason = "";
        var justifyArchivalDialog = (customMessage, action) => {
            var message = customMessage || Messages.admin_archiveReason;
            justifyDialog(message, archiveReason, reason => { archiveReason = reason; }, action);
        };

        var restoreReason = "";
        var justifyRestorationDialog = (customMessage, action) => {
            var message = customMessage || Messages.admin_restoreReason;
            justifyDialog(message, restoreReason, reason => { restoreReason = reason; }, action);
        };

        var copyToClipboard = (content) => {
            var button = blocks.activeButton('primary','', Messages.copyToClipboard, () => {
                var toCopy = JSON.stringify(content, null, 2);
                Clipboard.copy(toCopy, (err) => {
                    if (err) { return UI.warn(Messages.error); }
                    UI.log(Messages.genericCopySuccess);
                });
            }, true);
            return button;
        };

        var reportContentLabel = () => {
            return h('span', [
                Messages.admin_reportContent,
                ' (JSON) ',
                h('br'),
                h('small', Messages.ui_experimental),
            ]);
        };

        var DOCUMENT_TYPES = {
            32: 'channel',
            48: 'file',
            33: 'ephemeral',
            34: 'broadcast',
        };

        var inferDocumentType = id => {
            return DOCUMENT_TYPES[typeof(id) === 'string' && id.length] || 'unknown';
        };

        var renderAccountData = function (data) {
            var tableObj = makeMetadataTable('cp-account-stats');
            var row = tableObj.row;

            // info
            row(Messages.admin_generatedAt, new Date(data.generated));

            // signing key
            if (data.key === data.safeKey) {
                row(Messages.settings_publicSigningKey, h('code', data.key));
            } else {
                row(Messages.settings_publicSigningKey, h('span', [
                    h('code', data.key),
                    ', ',
                    h('br'),
                    h('code', data.safeKey),
                ]));
            }

            if (data.first || data.latest) {
                // First pin activity time
                row(Messages.admin_firstPinTime, maybeDate(data.first));

                // last pin activity time
                row(Messages.admin_lastPinTime, maybeDate(data.latest));
            }

            // currently online
            row(Messages.admin_currentlyOnline, localizeState(data.currentlyOnline));

            // plan name
            row(Messages.admin_planName, data.plan || Messages.ui_none);

            // plan note
            row(Messages.admin_note, data.note || Messages.ui_none);

            // storage limit
            if (data.limit) { row(Messages.admin_planlimit, getPrettySize(data.limit)); }

            // data stored
            if (data.usage) { row(Messages.admin_storageUsage, getPrettySize(data.usage)); }

            // number of channels
            if (typeof(data.channel) === "number") {
                row(Messages.admin_channelCount, data.channels);
            }

            // number of files pinned
            if (typeof(data.channel) === "number") {
                row(Messages.admin_fileCount, data.files);
            }

            row(Messages.admin_pinLogAvailable, localizeState(data.live));

            // pin log archived
            row(Messages.admin_pinLogArchived, localizeState(data.archived));

            if (data.archiveReport) {
                row(Messages.admin_accountSuspended, localizeState(Boolean(data.archiveReport)));
            }
            if (data.archiveReport_formatted) {
                let button, pre;
                row(Messages.admin_accountReport, h('div', [
                    pre = h('pre', data.archiveReport_formatted),
                    button = blocks.activeButton('primary', '',
                            Messages.admin_accountReportFull, () => {
                        $(button).remove();
                        $(pre).html(JSON.stringify(data.archiveReport, 0, 2));
                    }, true)
                ]));
            }


            // actions
            if (data.archived && data.live === false && data.archiveReport) {
                let button = blocks.activeButton('primary', '',
                        Messages.ui_restore, () => {
                    justifyRestorationDialog('', reason => {
                        sframeCommand('RESTORE_ACCOUNT', {
                            key: data.key,
                            reason: reason,
                        }, function (err) {
                            if (err) {
                                console.error(err);
                                return void UI.warn(Messages.error);
                            }
                            UI.log(Messages.ui_success);
                        });
                    });
                }, true);
                row(Messages.admin_restoreAccount, button);
            }

            if (data.live === true) {
                var getPins = (done) => {
                    sframeCommand('GET_PIN_LIST', data.key, (err, pins) => {
                        done(!err && Array.isArray(pins));
                        if (err || !Array.isArray(pins)) {
                            console.error(err);
                            return void UI.warn(Messages.error);
                        }

                        var table = makeMetadataTable('cp-pin-list').table;
                        var row = id => {
                            var type = inferDocumentType(id);
                            table.appendChild(h('tr', [
                                h('td', h('code', id)),
                                h('td', type),
                            ]));
                        };

                        var P = pins.slice().sort((a, b) => a.length - b.length);
                        P.map(row);

                        UI.confirm(table, yes => {
                            if (!yes) { return; }
                            var content = P.join('\n');
                            Clipboard.copy(content, (err) => {
                                if (err) { return UI.warn(Messages.error); }
                                UI.log(Messages.genericCopySuccess);
                            });
                        }, {
                            wide: true,
                            ok: Messages.copyToClipboard,
                        });
                    });
                };

                // get full pin list
                row(Messages.admin_getPinList, blocks.activeButton('primary', '', Messages.ui_fetch, getPins));

                // get full pin history
                var getHistoryHandler = (done) => {
                    sframeCommand('GET_PIN_HISTORY', data.key, (err, history) => {
                        done(!err);
                        if (err) {
                            console.error('Error retrieving pin history:', err);
                            return void UI.warn(Messages.error);
                        }
                        UI.alert(history); // TODO NOT_IMPLEMENTED
                    });
                };
                var pinHistoryButton = blocks.activeButton('primary', '', Messages.ui_fetch, getHistoryHandler);
                disable($(pinHistoryButton));

                // TODO pin history is not implemented
                //row(Messages.admin_getFullPinHistory, pinHistoryButton);

                // archive pin log
                var archiveHandler = () => {
                    justifyArchivalDialog(Messages.admin_archiveAccountConfirm, reason => {
                        sframeCommand('ARCHIVE_ACCOUNT', {
                            key: data.key,
                            block: data.blockId,
                            reason: reason,
                        }, (err /*, response */) => {
                            //console.error(err);
                            if (err) {
                                console.error(err);
                                return void UI.warn(Messages.error);
                            }
                            UI.log(Messages.ui_success);
                        });
                    });
                };

                var archiveAccountLabel = h('span', [
                    Messages.admin_archiveAccount,
                    h('br'),
                    h('small', Messages.admin_archiveAccountInfo)
                ]);
                let archiveAccountButton = blocks.activeButton('danger', '',
                                Messages.admin_archiveButton, archiveHandler, true);
                row(archiveAccountLabel, archiveAccountButton);

                // archive owned documents
                /* // TODO not implemented
                var archiveDocuments = () => {
                    justifyRestorationDialog(Messages.admin_archiveDocumentsConfirm, reason => {
                        sframeCommand('ARCHIVE_OWNED_DOCUMENTS', {
                            key: data.key,
                            reason: reason,
                        }, (err, response) => {
                            if (err) { return void UI.warn(err); }
                            UI.log(response);
                        });
                    });
                };

                var archiveDocumentsButton = danger(Messages.admin_archiveButton, archiveDocuments);
                disable($(archiveDocumentsButton));
                row(Messages.admin_archiveOwnedAccountDocuments, archiveDocumentsButton);
             */
            }

            row(reportContentLabel, copyToClipboard(data));

            return tableObj.table;
        };

        // Msg.admin_enableembedsHint, .admin_enableembedsTitle
        sidebar.addCheckboxItem({
            key: 'enableembeds',
            getState: function () {
                return APP.instanceStatus.enableEmbedding;
            },
            query: function (val, setState) {
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'ADMIN_DECREE',
                    data: ['ENABLE_EMBEDDING', [val]]
                }, function (e, response) {
                    if (e || response.error) {
                        UI.warn(Messages.error);
                        console.error(e, response);
                    }
                    APP.updateStatus(function () {
                        setState(APP.instanceStatus.enableEmbedding);
                        flushCache();
                    });
                });
            },
        });

        // Msg.admin_forcemfaHint, .admin_forcemfaTitle
        sidebar.addCheckboxItem({
            key: 'forcemfa',
            getState: function () {
                return APP.instanceStatus.enforceMFA;
            },
            query: function (val, setState) {
                var isChecked = APP.instanceStatus.enforceMFA;
                function showConfirmation(isChecked, setState) {
                    const confirmationContent = isChecked ? Messages.admin_mfa_confirm_disable : Messages.admin_mfa_confirm_enable;
                    UI.confirm(confirmationContent, function (confirmed) {
                        if (!confirmed) {
                            // User canceled their changes, restore the checkbox value
                            setState(isChecked);
                            return;
                        }
                        // User confirmed their changes, call the command and update the state
                        sFrameChan.query('Q_ADMIN_RPC', {
                            cmd: 'ADMIN_DECREE',
                            data: ['ENFORCE_MFA', [val]]
                        }, function (e, response) {
                            if (e || response.error) {
                                UI.warn(Messages.error);
                                console.error(e, response);
                            } else {
                                APP.updateStatus(function () {
                                    setState(APP.instanceStatus.enforceMFA);
                                    flushCache();
                                });
                            }
                        });
                    });
                }
                showConfirmation(isChecked, setState);
            }
        });



        var getInstanceString = function (attr) {
            var val = APP.instanceStatus[attr];
            var type = typeof(val);
            switch (type) {
                case 'string':
                    return val || '';
                case 'object':
                    return val.default || '';
                default:
                    return '';
            }
        };

        // Msg.admin_emailHint, Msg.admin_emailTitle
        sidebar.addItem('email', function (cb){
            var input = blocks.input({
                type: 'email',
                value: ApiConfig.adminEmail || '',
                'aria-labelledby': 'cp-admin-email'
            });
            var $input = $(input);

            var button = blocks.activeButton('primary', '', Messages.settings_save, function (done) {
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'ADMIN_DECREE',
                    data: ['SET_ADMIN_EMAIL', [$input.val().trim()]]
                }, function (e, response) {
                    if (e || response.error) {
                        UI.warn(Messages.error);
                        $input.val('');
                        console.error(e, response);
                        done(false);
                        return;
                    }
                    flushCache();
                    done(true);
                    UI.log(Messages._getKey('ui_saved', [Messages.admin_emailTitle]));
                });
            });
            var nav = blocks.nav([button]);

            var form = blocks.form([
                input,
            ], nav);

            $(nav).append(button.spinner);

            cb(form);
        });

        sidebar.addItem('apps', function (cb) {
            const appsToDisable = ApiConfig.appsToDisable || [];
            const grid = Onboarding.createAppsGrid(appsToDisable);

            var save = blocks.activeButton('primary', '', Messages.settings_save, function (done) {
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'ADMIN_DECREE',
                    data: ['DISABLE_APPS', appsToDisable]
                }, function (e, response) {
                    if (e || response.error) {
                        UI.warn(Messages.error);
                        console.error(e, response);
                        done(false);
                        return;
                    }
                    flushCache();
                    done(true);
                    UI.log(Messages._getKey('ui_saved', [Messages.admin_appSelection]));
                });
            });

            let form = blocks.form([
                grid
            ], blocks.nav([save]));

            cb(form);
        });


        sidebar.addItem('instance-info-notice', function(cb){
            var key = 'instance-info-notice';
            var notice = blocks.alert('info', key, [Messages.admin_infoNotice1, ' ', Messages.admin_infoNotice2]);
            cb(notice);
        },  {
            noTitle: true,
            noHint: true
        });

        // Msg.admin_nameHint, Msg.admin_nameTitle, Msg.admin_nameButton
        sidebar.addItem('name', function (cb){
            var input = blocks.input({
                type: 'text',
                value: getInstanceString('instanceName')|| ApiConfig.httpUnsafeOrigin || '',
                placeholder: ApiConfig.httpUnsafeOrigin,
                'aria-labelledby': 'cp-admin-name'
            });
            var $input = $(input);

            var button = blocks.activeButton('primary', '', Messages.settings_save, function (done) {
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'ADMIN_DECREE',
                    data: ['SET_INSTANCE_NAME', [$input.val().trim()]]
                }, function (e, response) {
                    if (e || response.error) {
                        UI.warn(Messages.error);
                        $input.val('');
                        console.error(e, response);
                        done(false);
                        return;
                    }
                    flushCache();
                    done(true);
                    UI.log(Messages._getKey('ui_saved', [Messages.admin_nameTitle]));
                });
            });

            var nav = blocks.nav([button]);
            var form = blocks.form([
                input,
            ], nav);

            $(nav).append(button.spinner);

            cb(form);
        });

        // Msg.admin_descriptionHint, Msg.admin_descriptionTitle
        sidebar.addItem('description', function (cb){
            var textarea = blocks.textarea({
                placeholder: Messages.home_host || '',
                'aria-labelledby': 'cp-admin-description'
            }, getInstanceString('instanceDescription'));
            var $input = $(textarea);
            var button = blocks.activeButton('primary', '', Messages.settings_save, function (done) {
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'ADMIN_DECREE',
                    data: ['SET_INSTANCE_DESCRIPTION', [$input.val().trim()]]
                }, function (e, response) {
                    if (e || response.error) {
                        UI.warn(Messages.error);
                        $input.val('');
                        console.error(e, response);
                        done(false);
                        return;
                    }
                    flushCache();
                    done(true);
                    UI.log(Messages._getKey('ui_saved', [Messages.admin_descriptionTitle]));
                });
            });

            var nav = blocks.nav([button]);
            $(nav).append(button.spinner);

            var form = blocks.form([
                textarea,
            ], nav);

            cb(form);
        });

        // Msg.admin_jurisdictionHint, Msg.admin_jurisdictionTitle
        sidebar.addItem('jurisdiction', function (cb){
            var input = blocks.input({
                type: 'text',
                value: getInstanceString('instanceJurisdiction'),
                placeholder: Messages.owner_unknownUser || '',
                'aria-labelledby': 'cp-admin-jurisdiction'
            });
            var $input = $(input);

            var button = blocks.activeButton('primary', '', Messages.settings_save, function (done) {
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'ADMIN_DECREE',
                    data: ['SET_INSTANCE_JURISDICTION', [$input.val().trim()]]
                }, function (e, response) {
                    if (e || response.error) {
                        UI.warn(Messages.error);
                        $input.val('');
                        console.error(e, response);
                        done(false);
                        return;
                    }
                    flushCache();
                    done(true);
                    UI.log(Messages._getKey('ui_saved', [Messages.admin_jurisdictionTitle]));
                });
            });

            var nav = blocks.nav([button]);
            $(nav).append(button.spinner);

            var form = blocks.form([
                input,
            ], nav);

            cb(form);
        });

        // Messages.admin_noticeHint, Messages.admin_noticeTitle
        sidebar.addItem('notice', function (cb){
            var input = blocks.input({
                type: 'text',
                value: getInstanceString('instanceNotice'),
                placeholder: '',
                'aria-labelledby': 'cp-admin-notice'
            });
            var $input = $(input);

            var button = blocks.activeButton('primary', '', Messages.settings_save, function (done) {
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'ADMIN_DECREE',
                    data: ['SET_INSTANCE_NOTICE', [$input.val().trim()]]
                }, function (e, response) {
                    if (e || response.error) {
                        UI.warn(Messages.error);
                        $input.val('');
                        console.error(e, response);
                        done(false);
                        return;
                    }
                    flushCache();
                    done(true);
                    UI.log(Messages._getKey('ui_saved', [Messages.admin_noticeTitle]));
                });
            });

            var nav = blocks.nav([button]);
            $(nav).append(button.spinner);

            var form = blocks.form([
                input,
            ], nav);

            cb(form);
        });

        // Msg.admin_logoHint, Msg.admin_logoTitle
        sidebar.addItem('logo', (cb) => {

            let input = blocks.input({
                type: 'file',
                accept: 'image/*',
                'aria-labelledby': 'cp-admin-logo'
            });

            var currentContainer = blocks.block([], 'cp-admin-customize-logo');
            let redraw = () => {
                var current = h('img', {src: '/api/logo?'+(+new Date()),alt:'Custom logo'});
                $(currentContainer).empty().append(current);
            };
            redraw();

            var upload = blocks.button('primary', '', Messages.admin_logoButton);
            var remove = blocks.button('danger', '', Messages.admin_logoRemoveButton);

            let spinnerBlock = blocks.inline();
            let spinner = UI.makeSpinner($(spinnerBlock));
            let form = blocks.form([
                currentContainer,
                blocks.block(input),
                blocks.nav([upload, remove, spinnerBlock])
            ]);

            let $button = $(upload);
            let $remove = $(remove);

            Util.onClickEnter($button, function () {
                let files = input.files;
                if (files.length !== 1) {
                    UI.warn(Messages.error);
                    return;
                }
                spinner.spin();
                $button.attr('disabled', 'disabled');
                let reader = new FileReader();
                reader.onloadend = function () {
                    let dataURL = this.result;
                    sframeCommand('UPLOAD_LOGO', {dataURL}, (err, response) => {
                        $button.removeAttr('disabled');
                        if (err) {
                            if(err === 'E_TOO_LARGE') {
                                UI.warn(Messages.admin_logoSize_error);
                            }
                            else{
                                UI.warn(Messages.error);
                            }
                            $(input).val('');
                            console.error(err, response);
                            spinner.hide();
                            return;
                        }
                        redraw();
                        spinner.done();
                        UI.log(Messages.saved);
                    });
                };
                reader.readAsDataURL(files[0]);
            });
            UI.confirmButton($remove, {
                classes: 'btn-danger',
                multiple: true
            }, function () {
                spinner.spin();
                $remove.attr('disabled', 'disabled');
                sframeCommand('REMOVE_LOGO', {}, (err, response) => {
                    $remove.removeAttr('disabled');
                    if (err) {
                        UI.warn(Messages.error);
                        console.error(err, response);
                        spinner.hide();
                        return;
                    }
                    redraw();
                    spinner.done();
                    UI.log(Messages.saved);
                });
            });

            cb(form);
        });

        // Msg.admin_colorHint, Msg.admin_colorTitle
        sidebar.addItem('color', cb => {
            let input = blocks.input({
                type: 'color',
                value: (Instance && Instance.color) || '#0087FF'
            });
            let label = blocks.labelledInput(Messages.admin_colorPick, input);
            let current = blocks.block([], 'cp-admin-color-current');
            let labelCurrent = blocks.labelledInput(Messages.admin_colorCurrent, current);
            let preview = blocks.block([
                blocks.block([
                    blocks.link('CryptPad', '/admin/#customize'),
                    blocks.button('primary', 'fa-floppy-o', Messages.settings_save),
                    blocks.button('secondary', 'fa-floppy-o', Messages.settings_save)
                ], 'cp-admin-color-preview-dark cp-sidebar-flex-block'),
                blocks.block([
                    blocks.link('CryptPad', '/admin/#customize'),
                    blocks.button('primary', 'fa-floppy-o', Messages.settings_save),
                    blocks.button('secondary', 'fa-floppy-o', Messages.settings_save)
                ], 'cp-admin-color-preview-light cp-sidebar-flex-block')
            ], 'cp-admin-color-preview');
            let labelPreview = blocks.labelledInput(Messages.admin_colorPreview, preview);
            let $preview = $(preview);

            let remove = blocks.button('danger', '', Messages.admin_logoRemoveButton);
            let $remove = $(remove);

            let setColor = (color, done) => {
                sframeCommand('CHANGE_COLOR', {color}, (err, response) => {
                    if (err) {
                        UI.warn(Messages.error);
                        console.error(err, response);
                        done(false);
                        return;
                    }
                    done(true);
                    UI.log(Messages.saved);
                });
            };

            let $input = $();
            let onColorPicked = () => {
                require(['/lib/less.min.js'], (Less) => {
                    let color = $input.val();
                    let lColor = Less.color(color.slice(1));
                    let lighten = Less.functions.functionRegistry._data.lighten;
                    let lightColor = lighten(lColor, {value:30}).toRGB();
                    $preview.find('.btn-primary').css({
                        'background-color': color
                    });
                    $preview.find('.cp-admin-color-preview-dark .btn-secondary').css({
                        'border-color': lightColor,
                        'color': lightColor,
                    });
                    $preview.find('.cp-admin-color-preview-light .btn-secondary').css({
                        'border-color': color,
                        'color': color,
                    });
                    $preview.find('.cp-admin-color-preview-dark a').attr('style', `color: ${lightColor} !important`);
                    $preview.find('.cp-admin-color-preview-light a').attr('style', `color: ${color} !important`);
                });
            };
            $input = $(input).on('change', onColorPicked).addClass('cp-admin-color-picker');

            let btn = blocks.activeButton('primary', '',
              Messages.admin_colorChange, (done) => {
                let color = $input.val();
                setColor(color, done);
            });


            UI.confirmButton($remove, {
                classes: 'btn-danger',
                multiple: true
            }, function () {
                $remove.attr('disabled', 'disabled');
                setColor('', () => {});
            });

            var colors = UIElements.makePalette(4, (color, $color) => {
                // onselect
                let rgb = $color.css('background-color');
                let hex = Util.rgbToHex(rgb);
                $input.val(hex);
                onColorPicked();
            });

            $(label).append(colors);
            let form = blocks.form([
                labelCurrent,
                label,
            ], blocks.nav([btn, remove, btn.spinner]));

            cb([form, labelPreview]);
        });

        // Msg.admin_registrationHint, .admin_registrationTitle
        // Msg.admin_registrationSsoTitle
        sidebar.addItem('registration', function(cb){
            var refresh = function () {};

            var restrict = blocks.activeCheckbox({
                key: 'registration',
                getState: function () {
                    return APP.instanceStatus.restrictRegistration;
                },
                query: function (val, setState) {
                    sFrameChan.query('Q_ADMIN_RPC', {
                        cmd: 'ADMIN_DECREE',
                        data: ['RESTRICT_REGISTRATION', [val]]
                    }, function (e, response) {
                        if (e || response.error) {
                            UI.warn(Messages.error);
                            console.error(e, response);
                        }
                        APP.updateStatus(function () {
                            setState(APP.instanceStatus.restrictRegistration);
                            refresh();
                            flushCache();
                        });
                    });
                },
            });

            var restrictSSO = blocks.activeCheckbox({
                key: 'registration-sso',
                getState: function () {
                    return APP.instanceStatus.restrictSsoRegistration;
                },
                query: function (val, setState) {
                    sFrameChan.query('Q_ADMIN_RPC', {
                        cmd: 'ADMIN_DECREE',
                        data: ['RESTRICT_SSO_REGISTRATION', [val]]
                    }, function (e, response) {
                        if (e || response.error) {
                            UI.warn(Messages.error);
                            console.error(e, response);
                        }
                        APP.updateStatus(function () {
                            setState(APP.instanceStatus.restrictSsoRegistration);
                            flushCache();
                        });
                    });
                }
            });
            var ssoEnabled = ApiConfig.sso && ApiConfig.sso.list && ApiConfig.sso.list.length;
            if (!ssoEnabled) { restrictSSO = undefined; }

            var $sso = $(restrictSSO);
            refresh = () => {
                var closed = APP.instanceStatus.restrictRegistration;
                if (closed) {
                    $sso.show();
                } else {
                    $sso.hide();
                }
            };
            refresh();

            cb(blocks.form([restrict, restrictSSO], []));
        });

        // Msg.admin_invitationHint, admin_invitationTitle
        sidebar.addItem('invitation', function(cb){
            var button = blocks.button('primary', '', Messages.admin_invitationCreate);
            var $b = $(button);

            var inputAlias = blocks.input({
                type: 'text'
            });
            var blockAlias = blocks.labelledInput(Messages.admin_invitationAlias, inputAlias);

            var inputEmail = blocks.input({
                type: 'email'
            });
            var blockEmail = blocks.labelledInput(Messages.admin_invitationEmail, inputEmail);

            var refreshInvite = function () {};
            var refreshButton = blocks.button('secondary', '', Messages.oo_refresh);
            Util.onClickEnter($(refreshButton), function () {
                refreshInvite();
            });

            var header = [
                Messages.admin_invitationLink,
                Messages.admin_invitationAlias,
                Messages.admin_invitationEmail,
                Messages.admin_documentCreationTime,
                ""
            ];
            var list = blocks.table(header, []);
            list.setAttribute('id', 'cp-admin-table');
            let div = blocks.block([list]);
            div.setAttribute('id', 'cp-admin-table-container');

            var nav = blocks.nav([button, refreshButton]);
            var form = blocks.form([
                blockAlias,
                blockEmail
            ], nav);

            var metadataMgr = common.getMetadataMgr();
            var privateData = metadataMgr.getPrivateData();

            var deleteInvite = function (id) {
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'DELETE_INVITATION',
                    data: id
                }, function (e, response) {
                    $b.prop('disabled', false);
                    if (e || response.error) {
                        UI.warn(Messages.error);
                        return void console.error(e, response);
                    }
                    refreshInvite();
                });
            };

            let sort = all => {
                return (k1, k2) => {
                    return all[k1].time - all[k2].time;
                };
            };
            refreshInvite = function () {
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'GET_ALL_INVITATIONS',
                }, function (e, response) {
                    if (e || response.error) {
                        if (!response || response.error !== "ENOENT") { UI.warn(Messages.error); }
                        console.error(e, response);
                        return;
                    }
                    if (!Array.isArray(response)) { return; }
                    var all = response[0];
                    var newEntries = [];

                    Object.keys(all).sort(sort(all)).forEach(function (key) {
                        var data = all[key];
                        var url = privateData.origin + Hash.hashToHref(key, 'register');

                        var del = blocks.button('danger', 'fa-trash', Messages.kanban_delete );
                        var $del = $(del);
                        Util.onClickEnter($del, function () {
                            $del.attr('disabled', 'disabled');
                            UI.confirm(Messages.admin_invitationDeleteConfirm, function (yes) {
                                $del.attr('disabled', '');
                                if (!yes) { return; }
                                deleteInvite(key);
                            });
                        });
                        var copy = blocks.button('secondary', 'fa-clipboard', Messages.admin_invitationCopy);
                        Util.onClickEnter($(copy), function () {
                            Clipboard.copy(url, () => {
                                UI.log(Messages.genericCopySuccess);
                            });
                        });

                        newEntries.push([
                            UI.dialog.selectable(url),
                            data.alias,
                            data.email,
                            new Date(data.time).toLocaleString(),
                            [copy, del]
                        ]);
                    });
                    list.updateContent(newEntries);

                });

            };
            refreshInvite();

            Util.onClickEnter($b, function () {
                var alias = $(inputAlias).val().trim();
                if (!alias) { return void UI.warn(Messages.error); } // FIXME better error message
                $b.prop('disabled', true);
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'CREATE_INVITATION',
                    data: {
                        alias,
                        email: $(inputEmail).val()
                    }
                }, function (e, response) {
                    $b.prop('disabled', false);
                    if (e || response.error) {
                        UI.warn(Messages.error);
                        return void console.error(e, response);
                    }
                    $(inputAlias).val('').focus();
                    $(inputEmail).val('');
                    refreshInvite();
                });
            });

            cb([form, div]);
        });

        var getBlockId = (val) => {
            var url;
            try {
                url = new URL(val, ApiConfig.httpUnsafeOrigin);
            } catch (err) { }
            var getKey = function () {
                var parts = val.split('/');
                return parts[parts.length - 1];
            };
            var isValidBlockURL = function (url) {
                if (!url) { return; }
                return /* url.origin === ApiConfig.httpUnsafeOrigin && */ /^\/block\/.*/.test(url.pathname) && getKey().length === 44;
            };
            if (isValidBlockURL(url)) {
                return getKey();
            }
            return;
        };

        var getAccountData = function (key, _cb) {
            var cb = Util.once(Util.mkAsync(_cb));
            var data = {
                generated: +new Date(),
                key: key,
                safeKey: Util.escapeKeyCharacters(key),
            };

            return void nThen(function (w) {
                sframeCommand('GET_PIN_ACTIVITY', key, w((err, response) => {
                    if (err === 'ENOENT') { return; }
                    if (err || !response || !response[0]) {
                        console.error(err);
                        console.error(response);
                        UI.warn(Messages.error);
                    } else {
                        data.first = response[0].first;
                        data.latest = response[0].latest;
                        console.info(err, response);
                    }
                }));
            }).nThen(function (w) {
                sframeCommand('IS_USER_ONLINE', key, w((err, response) => {
                    console.log('online', err, response);
                    if (!Array.isArray(response) || typeof(response[0]) !== 'boolean') { return; }
                    data.currentlyOnline = response[0];
                }));
            }).nThen(function (w) {
                if (!data.first) { return; }
                sframeCommand('GET_USER_QUOTA', key, w((err, response) => {
                    if (err || !response) {
                        return void console.error('quota', err, response);
                    } else {
                        data.plan = response[1];
                        data.note = response[2];
                        data.limit = response[0];
                    }
                }));
            }).nThen(function (w) {
                if (!data.first) { return; }
                // storage used
                sframeCommand('GET_USER_TOTAL_SIZE', key, w((err, response) => {
                    if (err || !Array.isArray(response)) {
                        //console.error('size', err, response);
                    } else {
                        //console.info('size', response);
                        data.usage = response[0];
                    }
                }));
            }).nThen(function (w) {
                if (!data.first) { return; }
                // channels pinned
                // files pinned
                sframeCommand('GET_USER_STORAGE_STATS', key, w((err, response) => {
                    if (err || !Array.isArray(response) || !response[0]) {
                        UI.warn(Messages.error);
                        return void console.error('storage stats', err, response);
                    } else {
                        data.channels = response[0].channels;
                        data.files = response[0].files;
                    }
                }));
            }).nThen(function (w) { // pin log status (live, archived, unknown)
                sframeCommand('GET_PIN_LOG_STATUS', key, w((err, response) => {
                    if (err || !Array.isArray(response) || !response[0]) {
                        console.error('pin log status', err, response);
                        return void UI.warn(Messages.error);
                    } else {
                        console.info('pin log status', response);
                        data.live = response[0].live;
                        data.archived = response[0].archived;
                    }
                }));
            }).nThen(function (w) {
                if (data.first) { return; }
                // Account is probably deleted
                sframeCommand('GET_ACCOUNT_ARCHIVE_STATUS', {key}, w((err, response) => {
                    if (err || !Array.isArray(response) || !response[0]) {
                        console.error('account status', err, response);
                    } else {
                        console.info('account status', response);
                        data.archiveReport = response[0];
                    }
                }));
            }).nThen(function () {
                //console.log(data);
                try {
                    ['generated', 'first', 'latest'].forEach(k => {
                        var val = data[k];
                        if (typeof(val) !== 'number') { return; }
                        data[`${k}_formatted`] = new Date(val);
                    });
                    ['limit', 'usage'].forEach(k => {
                        var val = data[k];
                        if (typeof(val) !== 'number') { return; }
                        data[`${k}_formatted`] = getPrettySize(val);
                    });
                    if (data.archiveReport) {
                        let formatted = Util.clone(data.archiveReport);
                        formatted.channels = data.archiveReport.channels.length;
                        formatted.blobs = data.archiveReport.blobs.length;
                        data['archiveReport_formatted'] = JSON.stringify(formatted, 0, 2);
                    }
                } catch (err) {
                    console.error(err);
                }

                cb(void 0, data);
            });
        };

        // Msg.admin_usersHint, admin_usersTitle
        // Msg.admin_storeInvitedLabel
        // Msg.admin_storeSsoLabel
        sidebar.addItem('users', function(cb){

            var invited = blocks.activeCheckbox({
                key: 'store-invited',
                getState: function () {
                    return !APP.instanceStatus.dontStoreInvitedUsers;
                },
                query: function (val, setState) {
                    sFrameChan.query('Q_ADMIN_RPC', {
                        cmd: 'ADMIN_DECREE',
                        data: ['DISABLE_STORE_INVITED_USERS', [!val]]
                    }, function (e, response) {
                        if (e || response.error) {
                            UI.warn(Messages.error);
                            console.error(e, response);
                        }
                        APP.updateStatus(function () {
                            setState(!APP.instanceStatus.dontStoreInvitedUsers);
                            flushCache();
                        });
                    });
                }
            });

            var sso = blocks.activeCheckbox({
                key: 'store-sso',
                getState: function () {
                    return !APP.instanceStatus.dontStoreSSOUsers;
                },
                query: function (val, setState) {
                    sFrameChan.query('Q_ADMIN_RPC', {
                        cmd: 'ADMIN_DECREE',
                        data: ['DISABLE_STORE_SSO_USERS', [!val]]
                    }, function (e, response) {
                        if (e || response.error) {
                            UI.warn(Messages.error);
                            console.error(e, response);
                        }
                        APP.updateStatus(function () {
                            setState(!APP.instanceStatus.dontStoreSSOUsers);
                            flushCache();
                        });
                    });
                }
            });
            var ssoEnabled = ApiConfig.sso && ApiConfig.sso.list && ApiConfig.sso.list.length;

            var button = blocks.button('primary', '', Messages.admin_usersAdd);
            var $b = $(button);

            var userAlias = blocks.input({ type: 'text' });
            var blockAlias = blocks.labelledInput(Messages.admin_invitationAlias, userAlias);

            var userEmail = blocks.input({ type: 'email' });
            var blockEmail = blocks.labelledInput(Messages.admin_invitationEmail, userEmail);

            var userEdPublic = blocks.input({ type: 'key' });
            var blockEdPublic = blocks.labelledInput(Messages.admin_limitUser, userEdPublic);

            var userBlock = blocks.input({ type: 'text' });
            var blockUser = blocks.labelledInput(Messages.admin_usersBlock, userBlock);

            var refreshUsers = function () {};
            var refreshButton = blocks.button('secondary', '', Messages.oo_refresh);
            Util.onClickEnter($(refreshButton), function () {
                refreshUsers();
            });

            var header = [
                Messages.admin_invitationAlias,
                Messages.admin_invitationEmail,
                Messages.admin_limitUser,
                Messages.admin_documentCreationTime,
                ""
            ];
            var list = blocks.table(header, []);
            list.setAttribute('id', 'cp-admin-table');
            let div = blocks.block([list]);
            div.setAttribute('id', 'cp-admin-table-container');

            var nav = blocks.nav([button, refreshButton]);

            if (!ssoEnabled) { sso = undefined; }
            var form = blocks.form([
                invited,
                sso,
                blockAlias,
                blockEmail,
                blockEdPublic,
                blockUser
            ], nav);

            var deleteUser = function (id) {
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'DELETE_KNOWN_USER',
                    data: id
                }, function (e, response) {
                    if (e || response.error) {
                        UI.warn(Messages.error);
                        return void console.error(e, response);
                    }
                    refreshUsers();
                });
            };

            var updateUser = function (key, changes) {
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'UPDATE_KNOWN_USER',
                    data: {
                        edPublic: key,
                        changes: changes
                    }
                }, function (e, response) {
                    if (e || response.error) {
                        UI.warn(Messages.error);
                        return void console.error(e, response);
                    }
                    refreshUsers();
                });
            };
            let sort = all => {
                return (k1, k2) => {
                    return all[k2].time - all[k1].time;
                };
            };
            refreshUsers = function () {
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'GET_ALL_USERS',
                }, function (e, response) {
                    if (e || response.error) {
                        if (!response || response.error !== "ENOENT") { UI.warn(Messages.error); }
                        console.error(e, response);
                        return;
                    }
                    if (!Array.isArray(response)) { return; }
                    var all = response[0];
                    var newEntries = [];
                    Object.keys(all).sort(sort(all)).forEach(function (key) {
                        var data = all[key];
                        var editUser = () => {};
                        var del = blocks.button('danger', 'fa fa-trash', Messages.admin_usersRemove);
                        var $del = $(del);
                        Util.onClickEnter($del, function () {
                            $del.attr('disabled', 'disabled');
                            UI.confirm(Messages.admin_usersRemoveConfirm, function (yes) {
                                $del.attr('disabled', '');
                                if (!yes) { return; }
                                deleteUser(key);
                            });
                        });
                        var edit = blocks.activeButton('secondary', 'fa fa-pencil',
                                    Messages.tag_edit, () => { editUser(); }, true);

                        let aliasCell = blocks.inline(data.alias);
                        let emailCell = blocks.inline(data.email);
                        var actions = blocks.nav([edit, del]);

                        let $alias = $(aliasCell);
                        let $email = $(emailCell);
                        var $actions = $(actions);

                        editUser = () => {
                            var aliasInput = h('input');
                            var emailInput = h('input');
                            $(aliasInput).val(data.alias);
                            $(emailInput).val(data.email);
                            var save = blocks.button('primary', '', Messages.settings_save);
                            var cancel = blocks.button('secondary', '', Messages.cancel);
                            Util.onClickEnter($(save), function () {
                                var aliasVal = $(aliasInput).val().trim();
                                if (!aliasVal) { return void UI.warn(Messages.error); }
                                var changes = {
                                    alias: aliasVal,
                                    email: $(emailInput).val().trim()
                                };
                                updateUser(key, changes);
                            });
                            Util.onClickEnter($(cancel), function () {
                                refreshUsers();
                            });
                            $alias.html('').append(aliasInput);
                            $email.html('').append(emailInput);
                            $actions.html('').append([save, cancel]);
                        };

                        let infoBtn = blocks.activeButton('primary', 'fa fa-database',
                                Messages.admin_diskUsageButton, function (done) {
                            getAccountData(key, (err, data) => {
                                done(!err);
                                if (err) { return void console.error(err); }
                                var table = renderAccountData(data);
                                UI.alert(table, () => {}, {
                                    wide: true,
                                });
                            });
                        });
                        newEntries.push([
                            aliasCell,
                            emailCell,
                            [blocks.code(key), infoBtn],
                            new Date(data.time).toLocaleString(),
                            actions
                        ]);
                    });
                    list.updateContent(newEntries);
                });
            };
            refreshUsers();
            Util.onClickEnter($b, function () {
                var alias = $(userAlias).val().trim();
                if (!alias) { return void UI.warn(Messages.error); }
                $b.prop('disabled', true);

                var done = () => { $b.prop('disabled', false); };
                // TODO Get "block" from pin log?

                var keyStr = $(userEdPublic).val().trim();
                var edPublic = keyStr && Keys.canonicalize(keyStr);
                if (!edPublic) {
                    done();
                    return void UI.warn(Messages.admin_invalKey);
                }
                var block = getBlockId($(userBlock).val());

                var obj = {
                    alias,
                    email: $(userEmail).val(),
                    block: block,
                    edPublic: edPublic,
                };
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'ADD_KNOWN_USER',
                    data: obj
                }, function (e, response) {
                    done();
                    if (e || response.error) {
                        UI.warn(Messages.error);
                        return void console.error(e, response);
                    }
                    $(userAlias).val('').focus();
                    $(userEmail).val('');
                    $(userBlock).val('');
                    $(userEdPublic).val('');
                    refreshUsers();
                });
            });

            cb([form, div]);
        });

        // Msg.admin_defaultlimitHint, .admin_defaultlimitTitle
        sidebar.addItem('defaultlimit', function (cb) {
            var _limit = APP.instanceStatus.defaultStorageLimit;
            var _limitMB = Util.bytesToMegabytes(_limit);
            var limit = getPrettySize(_limit);

            var newLimit = blocks.input({
                type: 'number',
                min: 0,
                value: _limitMB,
                'aria-labelledby': 'cp-admin-defaultlimit'
            });
            var button = blocks.button('primary', '', Messages.admin_setlimitButton);
            var nav = blocks.nav([button]);
            var text = blocks.inline(Messages._getKey('admin_limit', [limit]));

            var form = blocks.form([
                text,
                newLimit
            ], nav);

            UI.confirmButton(button, {
                classes: 'btn-primary',
                multiple: true,
                validate: function () {
                    var l = parseInt($(newLimit).val());
                    if (isNaN(l)) {
                        return UI.warn(Messages.error_limit);
                    }
                    return true;
                }
            }, function () {
                var lMB = parseInt($(newLimit).val()); // Megabytes
                var l = lMB * 1024 * 1024; // Bytes
                var data = [l];
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'ADMIN_DECREE',
                    data: ['UPDATE_DEFAULT_STORAGE', data]
                }, function (e, response) {
                    if (e || response.error) {
                        UI.warn(Messages.error);
                        return void console.error(e, response);
                    }
                    var limit = getPrettySize(l);
                    $(text).text(Messages._getKey('admin_limit', [limit]));
                    UI.log(Messages.saved);
                });
            });

            cb(form);
        });

        // Msg.admin_setlimitHint, .admin_setlimitTitle
        sidebar.addItem('setlimit', function(cb){
            var user = blocks.input({ type:'text', class: 'cp-setlimit-user'});
            var userBlock = blocks.labelledInput(Messages.admin_limitUser, user);
            var $key = $(user);
            var limit = blocks.input({
                type: 'number',
                min: 0,
                value: 0,
                class: 'cp-setlimit-limit'
            });
            var limitBlock = blocks.labelledInput(Messages.admin_limitMB, limit);
            var note = blocks.input({ type: 'text', class: 'cp-setlimit-note' });
            var noteBlock = blocks.labelledInput(Messages.admin_limitSetNote, note);
            var $note = $(note);

            var remove = blocks.button('danger', '', Messages.fc_remove );
            var set = blocks.button('primary', '', Messages.admin_setlimitButton);

            var nav = blocks.nav([set, remove]);
            var form = blocks.form([
                userBlock,
                limitBlock,
                noteBlock
            ], nav);

            var getValues = function () {
                var key = $key.val();
                var _limit = parseInt($(limit).val());
                if (key.length !== 44) {
                    try {
                        var u = Keys.parseUser(key);
                        if (!u.domain || !u.user || !u.pubkey) {
                            return void UI.warn(Messages.admin_invalKey);
                        }
                    } catch (e) {
                        return void UI.warn(Messages.admin_invalKey);
                    }
                }
                if (isNaN(_limit) || _limit < 0) {
                    return void UI.warn(Messages.admin_invalLimit);
                }
                var _note = ($note.val() || "").trim();
                return {
                    key: key,
                    data: {
                        limit: _limit * 1024 * 1024,
                        note: _note,
                        plan: 'custom'
                    }
                };
            };
            UI.confirmButton(remove, {
                classes: 'btn-danger',
                multiple: true,
                validate: function () {
                    var obj = getValues();
                    if (!obj || !obj.key) { return false; }
                    return true;
                }
            }, function () {
                var obj = getValues();
                var data = [obj.key];
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'ADMIN_DECREE',
                    data: ['RM_QUOTA', data]
                }, function (e, response) {
                    if (e || response.error) {
                        UI.warn(Messages.error);
                        console.error(e, response);
                        return;
                    }
                    APP.refreshLimits();
                    $key.val('');
                });
            });

            Util.onClickEnter($(set), function () {
                var obj = getValues();
                if (!obj || !obj.key) { return; }
                var data = [obj.key, obj.data];
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'ADMIN_DECREE',
                    data: ['SET_QUOTA', data]
                }, function (e, response) {
                    if (e || response.error) {
                        UI.warn(Messages.error);
                        console.error(e, response);
                        return;
                    }
                    APP.refreshLimits();
                    $key.val('');
                    $note.val('');
                });
            });

            cb(form);
        });

        // Msg.admin_getlimitsHint, .admin_getlimitsTitle
        sidebar.addItem('getlimits', function(cb){
            var header = [
                Messages.settings_publicSigningKey,
                Messages.admin_planlimit,
                Messages.admin_planName,
                Messages.admin_note
            ];
            var table = blocks.table(header, []);
            table.setAttribute('id', 'cp-admin-table');
            let div = blocks.block([table]);
            div.setAttribute('id', 'cp-admin-table-container');
            let $table = $(table).hide();

            APP.refreshLimits = function () {
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'GET_LIMITS',
                }, function (e, data) {
                    $table.hide();
                    if (e) { return; }
                    if (!Array.isArray(data) || !data[0]) { return; }
                    $table.show();

                    var obj = data[0];
                    if (obj && (obj.message || obj.location)) {
                        delete obj.message;
                        delete obj.location;
                    }
                    var list = Object.keys(obj).sort(function (a, b) {
                        return obj[a].limit > obj[b].limit;
                    });

                    var content = list.map(function (key) {
                        var user = obj[key];
                        var limit = getPrettySize(user.limit);
                        var infoButton = blocks.button('primary','',  Messages.admin_diskUsageButton);
                        Util.onClickEnter($(infoButton), function () {
                             getAccountData(key, (err, data) => {
                                 if (err) { return void console.error(err); }
                                 var table = renderAccountData(data);
                                 UI.alert(table, () => {

                                 }, {
                                    wide: true,
                                 });
                             });
                        });

                        var keyEl = h('code.cp-limit-key', key);
                        $(keyEl).click(function () {
                            $('[data-item="setlimit"]').find('.cp-setlimit-user').val(key);
                            $('[data-item="setlimit"]').find('.cp-setlimit-limit').val(Math.floor(user.limit / 1024 / 1024));
                            $('[data-item="setlimit"]').find('.cp-setlimit-note').val(user.note);
                        });
                        var title = Messages._getKey('admin_limit', [limit]) + ', ' +
                            Messages._getKey('admin_limitPlan', [user.plan]) + ', ' +
                            Messages._getKey('admin_limitNote', [user.note]);
                        var attr = { title: title };
                        return [
                            [keyEl, infoButton],
                            {attr, content: limit},
                            {attr, content: user.plan},
                            {attr, content: user.note}
                        ];
                    });
                    table.updateContent(content);
                });
            };
            APP.refreshLimits();
            cb(div);
        });

        // Msg.admin_accountMetadataHint.admin_accountMetadataTitle
        sidebar.addItem('account-metadata', function(cb) {
            var input = blocks.input({
                type: 'text',
                placeholder: Messages.admin_accountMetadataPlaceholder,
                value: '',
            });
            var $input = $(input);

            var btn = blocks.button('primary', '', Messages.ui_generateReport);
            var $btn = $(btn);

            var nav = blocks.nav([btn]);
            var results = blocks.inline([]);

            var form = blocks.form([
                input
            ], nav);

            form.append(results);

            var pending = false;
            var getInputState = function () {
                var val = $input.val().trim();
                var key = Keys.canonicalize(val);
                var state = {
                    value: val,
                    key: key,
                    valid: Boolean(key),
                    pending: pending,
                };

                return state;
            };

            disable($btn);

            var setInterfaceState = function (state) {
                state = state || getInputState();
                var both = [$input, $btn];
                if (state.pending) {
                    both.forEach(disable);
                } else if (state.valid) {
                    both.forEach(enable);
                } else {
                    enable($input);
                    disable($btn);
                }
            };

            $input.on('keypress keyup change paste', function () {
                setTimeout(setInterfaceState);
            });

            Util.onClickEnter($btn, function () {
                if (pending) { return; }
                var state = getInputState();
                if (!state.valid) {
                    results.innerHTML = '';
                    return void UI.warn(Messages.error);
                }
                var key = state.key;
                pending = true;
                setInterfaceState();

                getAccountData(key, (err, data) => {
                    pending = false;
                    setInterfaceState();
                    if (!data) {
                        results.innerHTML = '';
                        return UI.warn(Messages.error);
                    }
                    var table = renderAccountData(data);
                    results.innerHTML = '';
                    results.appendChild(table);
                });
            });

            cb(form);

        });

        var getDocumentData = function (id, cb) {
            var data = {
                generated: +new Date(),
                id: id,
            };
            data.type = inferDocumentType(id);

            nThen(function (w) {
                if (data.type !== 'channel') { return; }
                sframeCommand('GET_STORED_METADATA', id, w(function (err, res) {
                    if (err) { return void console.error(err); }
                    if (!(Array.isArray(res) && res[0])) { return void console.error("NO_METADATA"); }
                    var metadata = res[0];
                    data.metadata = metadata;
                    data.created = Util.find(data, ['metadata', 'created']);
                }));
            }).nThen(function (w) {
                sframeCommand("GET_DOCUMENT_SIZE", id, w(function (err, res) {
                    if (err) { return void console.error(err); }
                    if (!(Array.isArray(res) && typeof(res[0]) === 'number')) {
                        return void console.error("NO_SIZE");
                    }
                    data.size = res[0];
                }));
            }).nThen(function (w) {
                if (data.type !== 'channel') { return; }
                sframeCommand('GET_LAST_CHANNEL_TIME', id, w(function (err, res) {
                    if (err) { return void console.error(err); }
                    if (!Array.isArray(res) || typeof(res[0]) !== 'number') { return void console.error(res); }
                    data.lastModified = res[0];
                }));
            }).nThen(function (w) {
                // whether currently open
                if (data.type !== 'channel') { return; }
                sframeCommand('GET_CACHED_CHANNEL_METADATA', id, w(function (err, res) {
                    //console.info("cached channel metadata", err, res);
                    if (err === 'ENOENT') {
                        data.currentlyOpen = false;
                        return;
                    }

                    if (err) { return void console.error(err); }
                    if (!Array.isArray(res) || !res[0]) { return void console.error(res); }
                    data.currentlyOpen = true;
                }));
            }).nThen(function (w) {
                // status (live, archived, unknown)
                if (!['channel', 'file'].includes(data.type)) { return; }
                sframeCommand('GET_DOCUMENT_STATUS', id, w(function (err, res) {
                    if (err) { return void console.error(err); }
                    if (!Array.isArray(res) || !res[0]) {
                        UI.warn(Messages.error);
                        return void console.error(err, res);
                    }
                    data.live = res[0].live;
                    data.archived = res[0].archived;
                    data.placeholder = res[0].placeholder;
                    //console.error("get channel status", err, res);
                }));
            }).nThen(function () {
                // for easy readability when copying to clipboard
                try {
                    ['generated', 'created', 'lastModified'].forEach(k => {
                        data[`${k}_formatted`] = new Date(data[k]);
                    });
                } catch (err) {
                    console.error(err);
                }

                cb(void 0, data);
            });
        };

        /* FIXME
            Messages.admin_getFullPinHistory = 'Pin history';
            Messages.admin_archiveOwnedAccountDocuments = "Archive this account's owned documents (not implemented)";
            Messages.admin_archiveOwnedDocumentsConfirm = "All content owned exclusively by this user will be archived. This means their documents, drive, and accounts will be made inaccessible.  This action cannot be undone. Please save the full pin list before proceeding to ensure individual documents can be restored.";
        */

        var localizeType = function (type) {
            var o = {
                channel: Messages.type.doc,
                file: Messages.type.file,
            };
            return o[type] || Messages.ui_undefined;
        };

        var renderDocumentData = function (data) {
            var tableObj = makeMetadataTable('cp-document-stats');
            var row = tableObj.row;

            row(Messages.admin_generatedAt, maybeDate(data.generated));
            row(Messages.documentID, h('code', data.id));
            row(Messages.admin_documentType, localizeType(data.type));
            row(Messages.admin_documentSize, data.size? getPrettySize(data.size): Messages.ui_undefined);

            if (data.type === 'channel') {
                try {
                    row(Messages.admin_documentMetadata, h('pre', JSON.stringify(data.metadata || {}, null, 2)));
                } catch (err2) {
                    UI.warn(Messages.error);
                    console.error(err2);
                }

                // actions
                // get raw metadata history
                var metadataHistoryButton = blocks.activeButton('primary', '', Messages.ui_fetch, done => {
                    sframeCommand('GET_METADATA_HISTORY', data.id, (err, result) => {
                        done(!err);
                        if (err) {
                            UI.warn(Messages.error);
                            return void console.error(err);
                        }
                        if (!Array.isArray(result)) {
                            UI.warn(Messages.error);
                            return void console.error("Expected an array");
                        }
                        var tableObj = makeMetadataTable('cp-metadata-history');
                        var row = items => {
                            tableObj.table.appendChild(h('tr', items.map(item => {
                                return h('td', item);
                            })));
                        };
                        var scroll = el => h('div.scroll', el);
                        result.forEach(item => {
                            var raw = JSON.stringify(item);
                            var time;
                            var last;
                            if (Array.isArray(item)) {
                                last = item[item.length - 1];
                                if (typeof(last) === 'number') { time = last; }
                            } else if (item && typeof(item) === 'object') {
                                time = item.created;
                            }
                            row([
                                h('small', maybeDate(time)), // time
                                scroll(h('code', raw)), // Raw
                            ]);
                        });

                        UI.confirm(tableObj.table, (yes) => {
                            if (!yes) { return; }
                            var content = result.map(line => JSON.stringify(line)).join('\n');
                            Clipboard.copy(content, (err) => {
                                if (err) { return UI.warn(Messages.error); }
                                UI.log(Messages.genericCopySuccess);
                            });
                        }, {
                            wide: true,
                            ok: Messages.copyToClipboard,
                        });
                    });
                });
                row(Messages.admin_getRawMetadata, metadataHistoryButton);

                row(Messages.admin_documentCreationTime, maybeDate(data.created));
                row(Messages.admin_documentModifiedTime, maybeDate(data.lastModified));
                row(Messages.admin_currentlyOpen, localizeState(data.currentlyOpen));
            }
            if (['file', 'channel'].includes(data.type)) {
                row(Messages.admin_channelAvailable, localizeState(data.live));
                row(Messages.admin_channelArchived, localizeState(data.archived));
            }

            if (data.type === 'file') {
                // TODO what to do for files?

            }

            if (data.placeholder) {
                console.warn('Placeholder code', data.placeholder);
                row(Messages.admin_channelPlaceholder, UI.getDestroyedPlaceholderMessage(data.placeholder));
            }

            if (data.live && data.archived) {
                let disableButtons;
                let restoreButton = blocks.activeButton('danger', '', Messages.admin_unarchiveButton, function () {
                    justifyRestorationDialog('', reason => {
                        nThen(function (w) {
                            sframeCommand('REMOVE_DOCUMENT', {
                                id: data.id,
                                reason: reason,
                            }, w(err => {
                                if (err) {
                                    w.abort();
                                    return void UI.warn(Messages.error);
                                }
                            }));
                        }).nThen(function () {
                            sframeCommand("RESTORE_ARCHIVED_DOCUMENT", {
                                id: data.id,
                                reason: reason,
                            }, (err /*, response */) => {
                                if (err) {
                                    console.error(err);
                                    return void UI.warn(Messages.error);
                                }
                                UI.log(Messages.restoredFromServer);
                                disableButtons();
                            });
                        });
                    });
                }, true);

                let archiveButton = blocks.activeButton('danger', '',Messages.admin_archiveButton, function () {
                    justifyArchivalDialog('', result => {
                        sframeCommand('ARCHIVE_DOCUMENT', {
                            id: data.id,
                            reason: result,
                        }, (err /*, response */) => {
                            if (err) {
                                console.error(err);
                                return void UI.warn(Messages.error);
                            }
                            UI.log(Messages.archivedFromServer);
                            disableButtons();
                        });
                    });
                }, true);

                disableButtons = function () {
                    [archiveButton, restoreButton].forEach(el => {
                        disable($(el));
                    });
                };

                row(h('span', [
                    Messages.admin_documentConflict,
                    h('br'),
                    h('small', Messages.ui_experimental),
                ]), h('span', [
                    h('div.alert.alert-danger.cp-admin-bigger-alert', [
                        Messages.admin_conflictExplanation,
                    ]),
                    h('p', [
                        restoreButton,
                        archiveButton,
                    ]),
                ]));
            } else if (data.live) {
            // archive
                var archiveDocumentButton = blocks.activeButton('danger', '' ,Messages.admin_archiveButton, function () {
                    justifyArchivalDialog('', result => {
                        sframeCommand('ARCHIVE_DOCUMENT', {
                            id: data.id,
                            reason: result,
                        }, (err /*, response */) => {
                            if (err) {
                                console.error(err);
                                return void UI.warn(Messages.error);
                            }
                            UI.log(Messages.archivedFromServer);
                            disable($(archiveDocumentButton));
                        });
                    });
                }, true);
                row(Messages.admin_archiveDocument, h('span', [
                    archiveDocumentButton,
                    h('small', Messages.admin_archiveHint),
                ]));
            } else if (data.archived) {
                var restoreDocumentButton = blocks.activeButton('primary', '',Messages.admin_unarchiveButton, function () {
                    justifyRestorationDialog('', reason => {
                        sframeCommand("RESTORE_ARCHIVED_DOCUMENT", {
                            id: data.id,
                            reason: reason,
                        }, (err /*, response */) => {
                            if (err) {
                                console.error(err);
                                return void UI.warn(Messages.error);
                            }
                            UI.log(Messages.restoredFromServer);
                            disable($(restoreDocumentButton));
                        });
                    });
                }, true);
                row(Messages.admin_restoreDocument, h('span', [
                    restoreDocumentButton,
                    h('small', Messages.admin_unarchiveHint),
                ]));
            }

            row(reportContentLabel, copyToClipboard(data));

            return tableObj.table;
        };

        // Msg.admin_documentsDeletionHint.admin_documentsDeletionTitle
        sidebar.addItem('documents-deletion', cb => {
            const textarea = blocks.textarea({
                'aria-labelledby': 'cp-admin-documents-deletion'
            });
            const $textarea = $(textarea);
            const archiveButton = blocks.activeButton('danger', '',
                    Messages.admin_archiveButton, () => {
                const $btn = $(archiveButton);
                justifyArchivalDialog('', result => {
                    const val = $textarea.val().trim();
                    const all = val.split('\n').filter(str => {
                        let type = DOCUMENT_TYPES[str.length];
                        return ['channel', 'file'].includes(type);
                    });
                    console.error(val);
                    console.error(result);
                    disable($btn);
                    sframeCommand('ARCHIVE_DOCUMENTS', {
                        list: all,
                        reason: result,
                    }, (err, arr) => {
                        const res = Array.isArray(arr) && arr[0];
                        enable($btn);
                        if (err) {
                            console.error(err);
                            return void UI.warn(Messages.error);
                        }
                        if (Array.isArray(res?.failed)
                            && res?.failed.length) {
                            console.error("Failed deletion:");
                            console.error(res?.failed);
                        }
                        $textarea.val('');
                        UI.log(Messages.archivedFromServer);
                    });
                });
            }, true);
            const nav = blocks.nav([archiveButton]);
            const div = blocks.form([textarea], nav);
            cb(div);
        });

        // Msg.admin_documentMetadataHint.admin_documentMetadataTitle
        sidebar.addItem('document-metadata', function(cb){
            var input = blocks.input({
                type: 'text',
                placeholder: Messages.admin_documentMetadataPlaceholder,
                value: ''
            });
            var $input = $(input);
            var passwordContainer = UI.passwordInput({
                id: 'cp-database-document-pw',
                placeholder: Messages.admin_archiveInput2,
            });
            var $passwordContainer = $(passwordContainer);
            var $password = $(passwordContainer).find('input');

            var getBlobId = pathname => {
                var parts;
                try {
                    if (typeof(pathname) !== 'string') { return; }
                    parts = pathname.split('/').filter(Boolean);
                    if (parts.length !== 3) { return; }
                    if (parts[0] !== 'blob') { return; }
                    if (parts[1].length !== 2) { return; }
                    if (parts[2].length !== 48) { return; }
                    if (!parts[2].startsWith(parts[1])) { return; }
                } catch (err) { return false; }
                return parts[2];
            };

            var pending = false;
            var getInputState = function () {
                var val = $input.val().trim();
                var state = {
                    valid: false,
                    passwordRequired: false,
                    id: undefined,
                    input: val,
                    password: $password.val().trim(),
                    pending: false,
                };

                if (!val) { return state; }
                if (isHex(val) && [32, 48].includes(val.length)) {
                    state.valid = true;
                    state.id = val;
                    return state;
                }

                var url;
                try {
                    url = new URL(val, ApiConfig.httpUnsafeOrigin);
                } catch (err) {}

                if (!url) { return state; } // invalid

                // recognize URLs of the form: /blob/f1/f1338921fe8a73ed5401780d2147f725deeb9e3329f0f01e
                var blobId = getBlobId(url.pathname);
                if (blobId) {
                    state.valid = true;
                    state.id = blobId;
                    return state;
                }

                var parsed = Hash.isValidHref(val);
                if (!parsed || !parsed.hashData) { return state; }
                if (parsed.hashData.version === 3) {
                    state.id = parsed.hashData.channel;
                    state.valid = true;
                    return state;
                }

                var secret;
                if (parsed.hashData.password) {
                    state.passwordRequired = true;
                    secret = Hash.getSecrets(parsed.type, parsed.hash, state.password);
                } else {
                    secret = Hash.getSecrets(parsed.type, parsed.hash);
                }
                if (secret && secret.channel) {
                    state.id = secret.channel;
                    state.valid = true;
                    return state;
                }
                return state;
            };

            var results = blocks.inline([]);

            var btn = blocks.button('primary', '', Messages.ui_generateReport);
            var $btn = $(btn);

            var nav = blocks.nav([btn]);
            var form = blocks.form([
                input,
                passwordContainer
            ], nav);
            form.append(results);
            $passwordContainer.hide();
            disable($btn);

            var setInterfaceState = function () {
                var state = getInputState();
                var all = [ $btn, $password, $input ];
                var text = [$password, $input];

                if (state.pending) {
                    all.forEach(disable);
                } else if (state.valid) {
                    all.forEach(enable);
                } else {
                    text.forEach(enable);
                    disable($btn);
                }
                if (state.passwordRequired) {
                    $passwordContainer.show();
                } else {
                    $passwordContainer.hide();
                }
            };

            $input.on('keypress keyup change paste', function () {
                setTimeout(setInterfaceState);
            });

            Util.onClickEnter($btn, function () {
                if (pending) { return; }
                pending = true;
                var state = getInputState();
                setInterfaceState(state);
                getDocumentData(state.id, function (err, data) {
                    pending = false;
                    setInterfaceState();
                    if (err) {
                        results.innerHTML = '';
                        return void UI.warn(err);
                    }
                    var table = renderDocumentData(data);
                    results.innerHTML = '';
                    results.appendChild(table);
                });
            });

            cb(form);
        });

        var getBlockData = function (key, _cb) {
            var cb = Util.once(Util.mkAsync(_cb));
            var data = {
                generated: +new Date(),
                key: key,
            };

            nThen(function (w) {
                sframeCommand('GET_DOCUMENT_STATUS', key, w((err, res) => {
                    if (err) {
                        console.error(err);
                        return void UI.warn(Messages.error);
                    }
                    if (!Array.isArray(res) || !res[0]) {
                        UI.warn(Messages.error);
                        return void console.error(err, res);
                    }
                    data.live = res[0].live;
                    data.archived = res[0].archived;
                    data.totp = res[0].totp;
                    data.placeholder = res[0].placeholder;
                }));
            }).nThen(function () {
                try {
                    ['generated'].forEach(k => {
                        data[`${k}_formatted`] = new Date(data[k]);
                    });
                } catch (err) {
                    console.error(err);
                }

                cb(void 0, data);
            });
        };

        var renderBlockData  = function (data) {
            var tableObj = makeMetadataTable('cp-block-stats');
            var row = tableObj.row;

            row(Messages.admin_generatedAt, maybeDate(data.generated));
            row(Messages.admin_blockKey, h('code', data.key));
            row(Messages.admin_blockAvailable, localizeState(data.live));
            row(Messages.admin_blockArchived, localizeState(data.archived));

            row(Messages.admin_totpEnabled, localizeState(Boolean(data.totp.enabled)));
            row(Messages.admin_totpRecoveryMethod, data.totp.recovery);

            if (data.live) {
                var archiveButton = blocks.activeButton('danger', '', Messages.ui_archive, function () {
                    justifyArchivalDialog('', reason => {
                        sframeCommand('ARCHIVE_BLOCK', {
                            key: data.key,
                            reason: reason,
                        }, (err, res) => {
                            if (err) {
                                console.error(err);
                                return void UI.warn(Messages.error);
                            }
                            disable($(archiveButton));
                            UI.log(Messages.ui_success);
                            console.log('archive block', err, res);
                        });
                    });
                }, true);
                row(Messages.admin_archiveBlock, archiveButton);
            }
            if (data.placeholder) {
                console.warn('Placeholder code', data.placeholder);
                row(Messages.admin_channelPlaceholder, UI.getDestroyedPlaceholderMessage(data.placeholder, true));
            }
            if (data.archived && !data.live) {
                var restoreButton = blocks.activeButton('danger', '', Messages.ui_restore, function () {
                    justifyRestorationDialog('', reason => {
                        sframeCommand('RESTORE_ARCHIVED_BLOCK', {
                            key: data.key,
                            reason: reason,
                        }, (err, res) => {
                            if (err) {
                                console.error(err);
                                return void UI.warn(Messages.error);
                            }
                            disable($(restoreButton));
                            console.log('restore archived block', err, res);
                            UI.log(Messages.ui_success);
                        });
                    });
                }, true);
                row(Messages.admin_restoreBlock, restoreButton);
            }

            row(reportContentLabel, copyToClipboard(data));

            return tableObj.table;
        };

        // Msg.admin_blockMetadataHint.admin_blockMetadataTitle
        sidebar.addItem('block-metadata', function(cb){
            var input = blocks.input({
                type: 'text',
                placeholder: Messages.admin_blockMetadataPlaceholder,
                value: ''
            });
            var $input = $(input);
            var btn = blocks.button('primary', '', Messages.ui_generateReport);
            var $btn = $(btn);
            disable($btn);

            var results = blocks.inline([]);
            var nav = blocks.nav([btn]);
            var form = blocks.form([
                input
            ], nav);
            form.append(results);

            var pending = false;
            var getInputState = function () {
                var val = $input.val().trim();
                var state = {
                    pending: pending,
                    valid: false,
                    value: val,
                    key: '',
                };

                var key = getBlockId(val);
                if (key) {
                    state.valid = true;
                    state.key = key;
                }
                return state;
            };
            var setInterfaceState = function () {
                var state = getInputState();
                var all = [$btn, $input];

                if (state.pending) {
                    all.forEach(disable);
                } else if (state.valid) {
                    all.forEach(enable);
                } else {
                    enable($input);
                    disable($btn);
                }
            };

            $input.on('keypress keyup change paste', function () {
                setTimeout(setInterfaceState);
            });

            Util.onClickEnter($btn, function () {
                if (pending) { return; }
                var state = getInputState();
                pending = true;
                setInterfaceState();
                getBlockData(state.key, (err, data) => {
                    pending = false;
                    setInterfaceState();
                    if (err || !data) {
                        results.innerHTML = '';
                        console.log(err, data);
                        return UI.warn(Messages.error);
                    }
                    var table = renderBlockData(data);
                    results.innerHTML = '';
                    results.appendChild(table);
                });
            });

            cb(form);

        });

        var renderTOTPData  = function (data) {
            var tableObj = makeMetadataTable('cp-block-stats');
            var row = tableObj.row;

            row(Messages.admin_generatedAt, maybeDate(data.generated));
            row(Messages.admin_blockKey, h('code', data.key));
            row(Messages.admin_blockAvailable, localizeState(data.live));

            if (!data.live || !data.totp) { return tableObj.table; }

            row(Messages.admin_totpCheck, localizeState(data.totpCheck));

            if (!data.totpCheck) { return tableObj.table; }

            row(Messages.admin_totpEnabled, localizeState(Boolean(data.totp.enabled)));
            if (data.totp && data.totp.enabled) {
                row(Messages.admin_totpRecoveryMethod, data.totp.recovery);
            }

            if (!data.totpCheck || !data.totp.enabled) { return tableObj.table; }

            // TOTP is enabled and the signature is correct: display "disable TOTP" button
            var disableButton = blocks.button('danger', '', Messages.admin_totpDisableButton);
            UI.confirmButton(disableButton, { classes: 'btn-danger' }, () => {
                sframeCommand('DISABLE_MFA', data.key, (err, res) => {
                    if (err) {
                        console.error(err);
                        return void UI.warn(Messages.error);
                    }
                    if (!Array.isArray(res) || !res[0] || !res[0].success) {
                        return UI.warn(Messages.error);
                    }
                    UI.log(Messages.ui_success);
                });

            });
            row(Messages.admin_totpDisable, disableButton);

            return tableObj.table;
        };

        var checkTOTPRequest = function (json) {
            var clone = Util.clone(json);
            delete clone.proof;

            var msg = Util.decodeUTF8(Sortify(clone));
            var sig = Util.decodeBase64(json.proof);
            var pub = Util.decodeBase64(json.blockId);
            return Nacl.sign.detached.verify(msg, sig, pub);
        };

        // Msg.admin_totpRecoveryHint.admin_totpRecoveryTitle
        sidebar.addItem('totp-recovery', function(cb){
            var textarea = blocks.textarea({
                id: 'textarea-input',
                'aria-labelledby': 'cp-admin-totp-recovery'
            });
            var $input = $(textarea);
            var btn = blocks.button('primary','', Messages.admin_totpDisable);
            var $btn = $(btn);
            var results = blocks.inline([]);

            var nav = blocks.nav([btn]);
            var form = blocks.form([
                textarea
            ], nav);
            form.append(results);
            disable($btn);

            var pending = false;
            var getInputState = function () {
                var val = $input.val().trim();
                var state = {
                    pending: pending,
                    value: undefined,
                    key: '',
                };

                var json;
                try { json = JSON.parse(val); } catch (err) { }
                if (!json || json.intent !== "Disable TOTP" || !json.blockId || json.blockId.length !== 44 ||
                !json.date || !json.proof) { return state; }

                state.value = json;
                state.key = json.blockId.replace(/\//g, '-');
                return state;
            };
            var setInterfaceState = function () {
                var state = getInputState();
                var all = [$btn, $input];

                if (state.pending) {
                    all.forEach(disable);
                } else {
                    all.forEach(enable);
                }
            };

            setInterfaceState();
            Util.onClickEnter($btn, function () {
                if (pending) { return; }
                var state = getInputState();
                if (!state.value) { return; }
                pending = true;
                setInterfaceState();
                getBlockData(state.key, (err, data) => {
                    pending = false;
                    setInterfaceState();
                    if (err || !data) {
                        results.innerHTML = '';
                        console.log(err, data);
                        return UI.warn(Messages.error);
                    }
                    var check = checkTOTPRequest(state.value);
                    if (!check) { UI.warn(Messages.admin_totpFailed); }
                    data.totpCheck = check;
                    var table = renderTOTPData(data);
                    results.innerHTML = '';
                    results.appendChild(table);
                });
            });

            cb(form);
        });

        var onRefreshStats = Util.mkEvent();

        sidebar.addItem('refresh-stats', function(cb){
            var btn = blocks.button('primary', '',  Messages.oo_refresh);
            var $btn = $(btn);
            Util.onClickEnter($btn, function () {
                onRefreshStats.fire();
            });
            cb(btn);
        }, {
            noTitle: true,
            noHint: true
        });

        // Msg.admin_uptimeTitle, .admin_uptimeHint
        sidebar.addItem('uptime', function(cb){
            var pre = blocks.pre(Messages.admin_uptimeTitle);
            var set = function () {
                var uptime = APP.instanceStatus.launchTime;
                if (typeof(uptime) !== 'number') { return; }
                pre.innerText = '';
                pre.innerText = new Date(uptime);
            };

            set();
            onRefreshStats.reg(function () {
                APP.updateStatus(set);
            });

            cb(pre);
        });

        // Msg.admin_activeSessionsHint, .admin_activeSessionsTitle
        sidebar.addItem('active-sessions', function(cb){
            var pre = blocks.pre('');
            var onRefresh = function () {
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'ACTIVE_SESSIONS',
                }, function (e, data) {
                    pre.innerText = '';
                    var total = data[0];
                    var ips = data[1];
                    pre.append(total + ' (' + ips + ')');
                });
            };
            onRefresh();
            onRefreshStats.reg(onRefresh);

            cb(pre);
        });

        // Msg.admin_activePadsHint, .admin_activePadsTitle
        sidebar.addItem('active-pads', function(cb){
            var pre = blocks.pre('');
            var onRefresh = function () {
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'ACTIVE_PADS',
                }, function (e, data) {
                    pre.innerText = '';
                    pre.append(String(data));
                });
            };
            onRefresh();
            onRefreshStats.reg(onRefresh);

            cb(pre);
        });

        // Msg.admin_openFilesHint, .admin_openFilesTitle
        sidebar.addItem('open-files', function(cb){
            var pre = blocks.pre('');
            var onRefresh = function () {
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'GET_FILE_DESCRIPTOR_COUNT',
                }, function (e, data) {
                    if (e || (data && data.error)) {
                        console.error(e, data);
                        pre.innerText = '';
                        pre.append(String(e || data.error));
                        return;
                    }
                    pre.innerText='';
                    pre.append(String(data));
                });
            };
            onRefresh();
            onRefreshStats.reg(onRefresh);

            cb(pre);
        });

        // Msg.admin_registeredHint, .admin_registeredTitle
        sidebar.addItem('registered', function(cb){
            var pre = blocks.pre('');
            var onRefresh = function () {
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'REGISTERED_USERS',
                }, function (e, arr) {
                    pre.innerText = '';
                    let data = arr[0];
                    pre.append(String(data.users));
                });
            };
            onRefresh();
            onRefreshStats.reg(onRefresh);

            cb(pre);
        });

        // Msg.admin_diskUsageHint, .admin_diskUsageTitle, .admin_diskUsageButton
        sidebar.addItem('disk-usage', function(cb){
            var button = blocks.button('primary', '', Messages.admin_diskUsageButton);
            var $button = $(button);
            var called = false;
            var nav = blocks.nav([button]);
            var content = blocks.table([], []);
            var form = blocks.form([
                content
            ], nav);

            Util.onClickEnter($button, function() {
                UI.confirm(Messages.admin_diskUsageWarning, function (yes) {
                    if (!yes) { return; }
                    $button.hide();
                    if (called) { return; }
                    called = true;
                    sFrameChan.query('Q_ADMIN_RPC', {
                        cmd: 'DISK_USAGE',
                    }, function (e, data) {
                        if (e) { return void console.error(e); }
                        var obj = data[0];
                        Object.keys(obj).forEach(function (key) {
                            var val = obj[key];
                            var unit = Util.magnitudeOfBytes(val);
                            if (unit === 'GB') {
                                obj[key] = Util.bytesToGigabytes(val) + ' GB';
                            } else if (unit === 'MB') {
                                obj[key] = Util.bytesToMegabytes(val) + ' MB';
                            } else {
                                obj[key] = Util.bytesToKilobytes(val) + ' KB';
                            }
                        });
                        let attr = {'class': 'cp-strong'};
                        let entries = Object.keys(obj).map(function (k) {
                            return [
                                {attr, content: (k === 'total' ? k : '/' + k)},
                                obj[k]
                            ];
                        });
                        content.updateContent(entries);
                    });
                });
            });
            cb(form);
        });

        let onRefreshSupportEvt = Util.mkEvent();
        let refreshSupport = () => {
            let moderators, supportKey;
            nThen((waitFor) => {
                APP.supportModule.execCommand('GET_PRIVATE_KEY', {}, waitFor((obj) => {
                    supportKey = obj && obj.curvePublic;
                }));
            }).nThen((waitFor) => {
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'GET_MODERATORS',
                    data: {}
                }, waitFor((e, response) => {
                    if (e || response.error) {
                        console.error(e || response.error);
                        UI.warn(Messages.error);
                        return;
                    }
                    moderators = response[0];
                }));
            }).nThen(() => {
                onRefreshSupportEvt.fire({moderators, supportKey});
            });
        };
        const getMyData = () => {
            let metadataMgr = common.getMetadataMgr();
            let priv = metadataMgr.getPrivateData();
            let user = metadataMgr.getUserData();
            return {
                name: user.name,
                edPublic: priv.edPublic,
                curvePublic: user.curvePublic,
                mailbox: user.notifications,
                profile: user.profile
            };
        };
        const getContactData = (curve) => {
            if (!curve) { return getMyData(); }
            let friends = common.getFriends(true);
            let f = friends[curve];
            return {
                name: f.displayName,
                edPublic: f.edPublic,
                curvePublic: f.curvePublic,
                mailbox: f.notifications,
                profile: f.profile
            };
        };
        let generateSupportKey = (moderatorsData, supportKey, cb) => {
            let edPublic = common.getMetadataMgr().getPrivateData().edPublic;
            if (supportKey && !moderatorsData[edPublic]) {
                UI.alert("A support key already exists. You must be a moderator to generate a new one or delete the existing support data.");
                return;
            }
            cb = cb || function () {};
            const onError = (waitFor, err, res) => {
                if (waitFor) { waitFor.abort(); }
                console.error(err, res);
                UI.warn(Messages.error);
                cb(err);
            };

            nThen((waitFor) => {
                // Add myself to moderator role if not already there
                let me = getContactData();
                if (moderatorsData[me.edPublic]) { return; }
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'ADD_MODERATOR',
                    data: me
                }, waitFor((e, response) => {
                    if (e) { console.error(e, response); }
                }));
            }).nThen((waitFor) => {
                // Copy chainpad doc and pin
                APP.supportModule.execCommand('ROTATE_KEYS', {}, waitFor((obj) => {
                    if (obj && obj.error) { return void onError(waitFor, obj.error); }
                    if (obj && obj.success && obj.noNotify) {
                        UI.alert(Messages.admin_supportRotateNotify);
                        onError(waitFor, 'NOTIFY_ERROR', obj);
                        return;
                    }
                    UI.log(Messages.saved);
                }));
            }).nThen(() => {
                refreshSupport();
            });
        };
        // Msg.admin_supportSetupHint, .admin_supportSetupTitle
        sidebar.addItem('support-setup', function (cb) {
            let content = blocks.block();
            const $div = $(content);
            const redraw = function (moderatorsData, supportKey) {
                $div.empty();

                const state = blocks.block([], 'cp-admin-support-state');
                const $state = $(state).appendTo($div);
                const button = blocks.activeButton('primary', '',
                                    Messages.admin_supportInit, done => {
                    generateSupportKey(moderatorsData, supportKey, (err) => {
                        done(!err);
                    });
                });
                const $button = $(button).appendTo($div);

                const delButton = blocks.activeButton('danger', '',
                                    Messages.admin_supportDelete, done => {
                    UI.confirm(Messages.admin_supportConfirm, function (yes) {
                        if (!yes) { return void done(false); }
                        // Send the decree, don't delete data locally, we just want to remove
                        // the support UI for the clients
                        APP.supportModule.execCommand('DISABLE_SUPPORT', {}, (obj) => {
                            done(!(obj && obj.error));
                            if (obj && obj.error) {
                                console.error(obj.error);
                                return void UI.warn(Messages.error);
                            }
                            UI.log(Messages.saved);
                            refreshSupport();
                        });
                    });
                });
                const $delButton = $(delButton).hide();

                const openButton = blocks.button('primary', '', Messages.admin_supportOpen);
                const $openButton = $(openButton).hide();
                Util.onClickEnter($(openButton), () => {
                    common.openURL('/moderation/');
                });

                let nav = blocks.nav([button, openButton, delButton,
                                      button.spinner, delButton.spinner]);
                $div.append(nav);

                const setState = function () {
                    $state.html('');
                    if (supportKey) {
                        $button.hide();
                        $delButton.show();
                        $openButton.show();
                        return $state.append([
                            blocks.icon('fa-check'),
                            blocks.inline(Messages.admin_supportEnabled)
                        ]);
                    }
                    $state.append([
                        blocks.icon('fa-times'),
                        blocks.inline(Messages.admin_supportDisabled)
                    ]);
                };
                setState();
            };
            onRefreshSupportEvt.reg(obj => {
                redraw(obj.moderators, obj.supportKey);
            });
            cb(content);
        });

        // Msg.admin_supportTeamHint, .admin_supportTeamTitle
        sidebar.addItem('support-team', function (cb) {
            let edPublic = common.getMetadataMgr().getPrivateData().edPublic; // My edPublic
            let content = blocks.block();
            const $div = $(content);
            const redraw = function (moderatorsData, supportKey) {
                $div.empty();

                const addModerator = (curve, _cb) => {
                    let cb = Util.mkAsync(_cb);
                    let userData = getContactData(curve);
                    if (!userData) { return void cb('INVALID_USER'); }
                    sFrameChan.query('Q_ADMIN_RPC', {
                        cmd: 'ADD_MODERATOR',
                        data: userData
                    }, (e, response) => {
                        if (e || (response && response.error)) {
                            return void cb(e || response.error);
                        }

                        // User added to support team in database, send them the keys
                        APP.supportModule.execCommand('ADD_MODERATOR', userData, cb);
                    });
                };
                const removeModerator = (ed) => {
                    sFrameChan.query('Q_ADMIN_RPC', {
                        cmd: 'REMOVE_MODERATOR',
                        data: ed
                    }, (e, response) => {
                        if (e || (response && response.error)) {
                            console.error(e || response.error);
                            return void UI.warn(Messages.error);
                        }
                        generateSupportKey(moderatorsData, supportKey);
                    });
                };

                const drawModerators = () => {
                    if (!supportKey) {
                        const list = blocks.block([
                            blocks.icon('fa-times'),
                            blocks.inline(Messages.admin_supportDisabled)
                        ], 'cp-admin-support-state');
                        $div.append(list);
                        return;
                    }
                    const members = {};
                    const friends = Util.clone(common.getFriends(false));
                    Object.keys(moderatorsData).forEach((ed) => {
                        let m = moderatorsData[ed];
                        members[m.curvePublic] = {
                            displayName: m.name,
                            edPublic: m.edPublic,
                            profile: m.profile,
                            curvePublic: m.curvePublic,
                            notificatons: m.mailbox
                        };
                    });
                    Object.keys(friends).forEach((curve) => {
                        if (members[curve]) { delete friends[curve]; }
                    });
                    let currentList = UIElements.getUserGrid(Messages.admin_supportMembers, {
                        common: common,
                        list: true,
                        large: true,
                        noSelect: true,
                        data: members,
                        remove: (el) => {
                            let ed = $(el).attr('data-ed');
                            removeModerator(ed);
                        }
                    });
                    let contactsGrid = UIElements.getUserGrid(Messages.admin_supportAdd, {
                        common: common,
                        list: true,
                        large: true,
                        data: friends
                    }, function () {});

                    let addBtn = blocks.button('primary', '', Messages.tag_add);
                    Util.onClickEnter($(addBtn), () => {
                        var $sel = $(contactsGrid.div).find('.cp-usergrid-user.cp-selected');
                        nThen((waitFor) => {
                            $sel.each((i, el) => {
                                let curve = $(el).attr('data-curve');
                                if (!curve) {
                                    console.error('Missing data on selected user', el);
                                    return void UI.warn(Messages.error);
                                }
                                addModerator(curve, waitFor());
                            });
                        }).nThen(() => {
                            refreshSupport();
                        });
                    });
                    // Only moderators can add new moderators
                    if (!moderatorsData[edPublic]) {
                        contactsGrid.div = undefined;
                        addBtn = undefined;
                    }

                    const list = blocks.block([
                        currentList.div,
                        contactsGrid.div,
                        blocks.nav([addBtn])
                    ]);
                    $div.append(list);
                };
                drawModerators();
            };
            onRefreshSupportEvt.reg(obj => {
                redraw(obj.moderators, obj.supportKey);
            });
            cb(content);
        });
        setTimeout(refreshSupport);

        var getApi = function (cb) {
            return function () {
                require(['/api/broadcast?'+ (+new Date())], function (Broadcast) {
                    cb(Broadcast);
                    setTimeout(function () {
                        try {
                            var ctx = require.s.contexts._;
                            var defined = ctx.defined;
                            Object.keys(defined).forEach(function (href) {
                                if (/^\/api\/broadcast\?[0-9]{13}/.test(href)) {
                                    delete defined[href];
                                    return;
                                }
                            });
                        } catch (e) {}
                    });
                });
            };
        };
        var checkLastBroadcastHash = function (cb) {
            var deleted = [];

            require(['/api/broadcast?'+ (+new Date())], function (BCast) {
                var hash = BCast.lastBroadcastHash || '1'; // Truthy value if no lastKnownHash
                common.mailbox.getNotificationsHistory('broadcast', null, hash, function (e, msgs) {
                    if (e) { console.error(e); return void cb(e); }

                    // No history, nothing to change
                    if (!Array.isArray(msgs)) { return void cb(); }
                    if (!msgs.length) { return void cb(); }

                    var lastHash;
                    var next = false;

                    // Start from the most recent messages until you find a CUSTOM message and
                    // check if it has been deleted
                    msgs.reverse().some(function (data) {
                        var c = data.content;

                        // This is the hash we want to keep
                        if (next) {
                            if (!c || !c.hash) { return; }
                            lastHash = c.hash;
                            next = false;
                            return true;
                        }

                        // initialize with the most recent hash
                        if (!lastHash && c && c.hash) { lastHash = c.hash; }

                        var msg = c && c.msg;
                        if (!msg) { return; }

                        // Remember all deleted messages
                        if (msg.type === "BROADCAST_DELETE") {
                            deleted.push(Util.find(msg, ['content', 'uid']));
                        }

                        // Only check custom messages
                        if (msg.type !== "BROADCAST_CUSTOM") { return; }

                        // If the most recent CUSTOM message has been deleted, it means we don't
                        // need to keep any message and we can continue with lastHash as the most
                        // recent broadcast message.
                        if (deleted.indexOf(msg.uid) !== -1) { return true; }

                        // We just found the oldest message we want to keep, move one iteration
                        // further into the loop to get the next message's hash.
                        // If this is the end of the loop, don't bump lastBroadcastHash at all.
                        next = true;
                    });

                    // If we don't have to bump our lastBroadcastHash, abort
                    if (next) { return void cb(); }

                    // Otherwise, bump to lastHash
                    console.warn('Updating last broadcast hash to', lastHash);
                    sFrameChan.query('Q_ADMIN_RPC', {
                        cmd: 'ADMIN_DECREE',
                        data: ['SET_LAST_BROADCAST_HASH', [lastHash]]
                    }, function (e, response) {
                        if (e || response.error) {
                            UI.warn(Messages.error);
                            console.error(e, response);
                            return;
                        }
                        console.log('lastBroadcastHash updated');
                        if (typeof(cb) === "function") { cb(); }
                    });
                });
            });

        };
        // Msg.admin_maintenanceHint, admin_maintenanceTitle
        sidebar.addItem('maintenance', function(cb){
            var button = blocks.button('primary', '', Messages.admin_maintenanceButton);
            var $button = $(button);
            var start = blocks.input({
                type: 'date',
                id: 'cp-admin-start-input',
                class: 'flatpickr-input'
            });
            var end = blocks.input({
                type: 'date',
                id: 'cp-admin-end-input',
                class: 'flatpickr-input'
            });
            var labelStart = blocks.labelledInput(Messages.broadcast_start, start);
            var labelEnd = blocks.labelledInput(Messages.broadcast_end, end);
            let active = blocks.block([], 'cp-broadcast-active');
            let $active = $(active);

            var form = blocks.form([
                active,
                labelStart,
                labelEnd,
            ], blocks.nav([button]));

            let send = function () {};
            var refresh = getApi(function (Broadcast) {
                $active.empty();
                var removeButton = blocks.button('danger', '', Messages.admin_maintenanceCancel);

                if (Broadcast && Broadcast.maintenance) {
                    var m = Broadcast.maintenance;
                    if (m.start && m.end && m.end >= (+new Date())) {
                        $active.append([
                            UI.setHTML(h('div'), Messages._getKey('broadcast_maintenance', [
                                new Date(m.start).toLocaleString(),
                                new Date(m.end).toLocaleString(),
                            ])),
                            blocks.nav([removeButton])
                        ]);
                    }
                }
                UI.confirmButton(removeButton, {
                    classes: 'btn-danger',
                }, function () {
                    send("");
                });

            });
            refresh();

            var $start = $(start);
            var $end = $(end);
            var is24h = UIElements.is24h();
            var dateFormat = "Y-m-d H:i";
            if (!is24h) { dateFormat = "Y-m-d h:i K"; }

            var endPickr = Flatpickr(end, {
                enableTime: true,
                time_24hr: is24h,
                dateFormat: dateFormat,
                minDate: new Date()
            });
            Flatpickr(start, {
                enableTime: true,
                time_24hr: is24h,
                minDate: new Date(),
                dateFormat: dateFormat,
                onChange: function () {
                    endPickr.set('minDate', new Date($start.val()));
                }
            });

            // Extract form data
            var getData = function () {
                var start = +new Date($start.val());
                var end = +new Date($end.val());
                if (isNaN(start) || isNaN(end)) {
                    console.error('Invalid dates');
                    return false;
                }
                return {
                    start: start,
                    end: end
                };
            };

            send = function (data) {
                disable($button);
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'ADMIN_DECREE',
                    data: ['SET_MAINTENANCE', [data]]
                }, function (e, response) {
                    enable($button);
                    if (e || response.error) {
                        UI.warn(Messages.error);
                        console.error(e, response);
                        $button.prop('disabled', '');
                        return;
                    }
                    // Maintenance applied, send notification
                    common.mailbox.sendTo('BROADCAST_MAINTENANCE', {}, {}, function () {
                        checkLastBroadcastHash(function () {
                            setTimeout(refresh, 300);
                        });
                    });
                });

            };
            Util.onClickEnter($(button), function () {
                var data = getData();
                if (data === false) { return void UI.warn(Messages.error); }
                send(data);
            });

            common.makeUniversal('broadcast', {
                onEvent: function (obj) {
                    var cmd = obj.ev;
                    if (cmd !== "MAINTENANCE") { return; }
                    refresh();
                }
            });

            cb(form);

        });

        // Msg.admin_surveyHint, admin_surveyTitle
        sidebar.addItem('survey', function(cb){
            var button = blocks.button('primary', '', Messages.admin_surveyButton);
            var $button = $(button);
            let nav = blocks.nav([button]);
            let active = blocks.block([], 'cp-sidebar-flex-block');
            let $active = $(active);

            var input = blocks.input({
                type: 'text',
                id: 'cp-admin-survey-url-input'
            });
            var labelledInput = blocks.labelledInput(Messages.broadcast_surveyURL, input);
            var $input = $(input);

            let send = function () {};
            var refresh = getApi(function (Broadcast) {
                var removeButton = blocks.button('danger', '', Messages.admin_surveyCancel);
                UI.confirmButton(removeButton, {
                    classes: 'btn-danger',
                }, function () {
                    send("");
                });

                $active.empty();
                if (Broadcast && Broadcast.surveyURL) {
                    var a = blocks.link(Messages.admin_surveyActive, Broadcast.surveyURL);
                    $active.append([a, removeButton]);
                }
            });
            refresh();

            send = function (data) {
                $button.prop('disabled', 'disabled');
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'ADMIN_DECREE',
                    data: ['SET_SURVEY_URL', [data]]
                }, function (e, response) {
                    $button.prop('disabled', '');
                    if (e || response.error) {
                        UI.warn(Messages.error);
                        console.error(e, response);
                        return;
                    }
                    // Maintenance applied, send notification
                    common.mailbox.sendTo('BROADCAST_SURVEY', {
                        url: data
                    }, {}, function () {
                        checkLastBroadcastHash(function () {
                            setTimeout(refresh, 300);
                        });
                    });
                });
            };
            // Extract form data
            var getData = function () {
                var url = $input.val();
                if (!Util.isValidURL(url)) {
                    console.error('Invalid URL', url);
                    return false;
                }
                return url;
            };
            Util.onClickEnter($(button), function () {
                var data = getData();
                if (data === false) { return void UI.warn(Messages.error); }
                send(data);
            });

            common.makeUniversal('broadcast', {
                onEvent: function (obj) {
                    var cmd = obj.ev;
                    if (cmd !== "SURVEY") { return; }
                    refresh();
                }
            });

            var form = blocks.form([active, labelledInput], nav);
            cb(form);
        });

        // Msg.admin_broadcastHint, admin_broadcastTitle
        sidebar.addItem('broadcast', function(cb) {
            var form = blocks.block([], 'cp-admin-broadcast-form');
            var $form = $(form);
            var refresh = getApi(function(Broadcast) {
                var button = blocks.button('primary', '', Messages.admin_broadcastButton);
                var $button = $(button);
                var removeButton = blocks.button('danger', '', Messages.admin_broadcastCancel);
                var activeContent = Messages.admin_broadcastActive;
                var active = blocks.block(blocks.inline(activeContent), 'cp-broadcast-active');
                var $active = $(active);
                var activeUid;
                var deleted = [];

                // Render active message (if there is one)
                var hash = Broadcast.lastBroadcastHash || '1'; // Truthy value if no lastKnownHash
                common.mailbox.getNotificationsHistory('broadcast', null, hash, function (e, msgs) {
                    if (e) { return void console.error(e); }
                    if (!Array.isArray(msgs)) { return; }
                    if (!msgs.length) {
                        $active.hide();
                    }
                    msgs.reverse().some(function (data) {
                        var c = data.content;
                        var msg = c && c.msg;
                        if (!msg) { return; }
                        if (msg.type === "BROADCAST_DELETE") {
                            deleted.push(Util.find(msg, ['content', 'uid']));
                        }
                        if (msg.type !== "BROADCAST_CUSTOM") { return; }
                        if (deleted.indexOf(msg.uid) !== -1) { return true; }

                        // We found an active custom message, show it
                        var el = common.mailbox.createElement(data);

                        var uid = Util.find(data, ['content', 'msg', 'uid']);
                        var time = Util.find(data, ['content', 'msg', 'content', 'time']);

                        var formattedTime = new Date(time || 0).toLocaleString();
                        var rowContent = [
                            'ID: ' + uid,
                            formattedTime,
                            $(el).find('.cp-notification-content').text(),
                            removeButton
                        ];
                        var table = blocks.table([], [rowContent]);
                        $active.append(table);
                        activeUid = uid;
                        return true;
                    });
                    if (!activeUid) { $active.hide(); }
                });

                // Custom message
                var container = blocks.block([], 'cp-broadcast-container');
                var $container = $(container);
                var languages = Messages._languages;
                var keys = Object.keys(languages).sort();

                // Always keep the textarea ordered by language code
                var reorder = function () {
                    $container.find('.cp-broadcast-lang').each(function (i, el) {
                        var $el = $(el);
                        var l = $el.attr('data-lang');
                        var index = keys.indexOf(l);
                        if (index !== -1) {
                            $el.css('order', index * 2);
                        } else {
                            console.error('Language key not found:', l);
                        }
                    });
                };
                // Remove a textarea
                var removeLang = function (l) {
                    $container.find('.cp-broadcast-lang[data-lang="'+l+'"]').remove();

                    var hasDefault = $container.find('.cp-broadcast-lang .cp-checkmark input:checked').length;
                    if (!hasDefault) {
                        $container.find('.cp-broadcast-lang').first().find('.cp-checkmark input').prop('checked', 'checked');
                    }
                };

                var getData = function () { return false; };
                var onPreview = function (l) {
                    var data = getData();
                    if (data === false) { return void UI.warn(Messages.error); }

                    var msg = {
                        uid: Util.uid(),
                        type: 'BROADCAST_CUSTOM',
                        content: data
                    };
                    common.mailbox.onMessage({
                        lang: l,
                        type: 'broadcast',
                        content: {
                            msg: msg,
                            hash: 'LOCAL|' + JSON.stringify(msg).slice(0,58)
                        }
                    }, function () {
                        UI.log(Messages.saved);
                    });
                };

                // Add a textarea
                var addLang = function (l) {
                    if ($container.find('.cp-broadcast-lang[data-lang="'+l+'"]').length) { return; }
                    var preview = blocks.button('secondary', '', Messages.broadcast_preview);
                    $(preview).click(function () {
                        onPreview(l);
                    });
                    var bcastDefault = Messages.broadcast_defaultLanguage;
                    var first = !$container.find('.cp-broadcast-lang').length;
                    var radio = UI.createRadio('broadcastDefault', null, bcastDefault, first, {
                        'data-lang': l,
                        label: {class: 'noTitle'}
                    });

                    var textarea = blocks.textarea();
                    var label = blocks.labelledInput(Messages.kanban_body, textarea);

                    $container.append(h('div.cp-broadcast-lang', { 'data-lang': l }, [
                        h('h4', languages[l]),
                        label,
                        radio,
                        preview
                    ]));

                    reorder();
                };
                 // Checkboxes to select translations
                var boxes = keys.map(function (l) {
                    var $cbox = $(UI.createCheckbox('cp-broadcast-custom-lang-'+l,
                        languages[l], false, { label: { class: 'noTitle' } }));
                    var $check = $cbox.find('input').on('change', function () {
                        var c = $check.is(':checked');
                        if (c) { return void addLang(l); }
                        removeLang(l);
                    });
                    if (l === 'en') {
                        setTimeout(function () {
                            $check.click();
                        });
                    }
                    return $cbox[0];
                });

                 // Extract form data
                getData = function () {
                    var map = {};
                    var defaultLanguage;
                    var error = false;
                    $container.find('.cp-broadcast-lang').each(function (i, el) {
                        var $el = $(el);
                        var l = $el.attr('data-lang');
                        if (!l) { error = true; return; }
                        var text = $el.find('textarea').val();
                        if (!text.trim()) { error = true; return; }
                        if ($el.find('.cp-checkmark input').is(':checked')) {
                            defaultLanguage = l;
                        }
                        map[l] = text;
                    });
                    if (!Object.keys(map).length) {
                        console.error('You must select at least one language');
                        return false;
                    }
                    if (error) {
                        console.error('One of the selected languages has no data');
                        return false;
                    }
                    return {
                        defaultLanguage: defaultLanguage,
                        content: map
                    };
                };

                var send = function (data) {
                    $button.prop('disabled', 'disabled');
                    //data.time = +new Date(); // FIXME not used anymore?
                    common.mailbox.sendTo('BROADCAST_CUSTOM', data, {}, function (err) {
                        if (err) {
                            $button.prop('disabled', '');
                            console.error(err);
                            return UI.warn(Messages.error);
                        }
                        UI.log(Messages.saved);
                        checkLastBroadcastHash(function () {
                            setTimeout(refresh, 300);
                        });
                    });
                };

                $button.click(function () {
                    var data = getData();
                    if (data === false) { return void UI.warn(Messages.error); }
                    send(data);
                });

                UI.confirmButton(removeButton, {
                    classes: 'btn-danger',
                }, function () {
                    if (!activeUid) { return; }
                    common.mailbox.sendTo('BROADCAST_DELETE', {
                        uid: activeUid
                    }, {}, function (err) {
                        if (err) { return UI.warn(Messages.error); }
                        UI.log(Messages.saved);
                        checkLastBroadcastHash(function () {
                            setTimeout(refresh, 300);
                        });
                    });
                });

                // Make the form
                $form.empty().append([
                    active,
                    h('div', Messages.broadcast_translations),
                    h('div.cp-broadcast-languages', boxes),
                    container,
                    h('div.cp-broadcast-form-submit', [
                        button
                    ])
                ]);
            });
            refresh();
            cb(form);

        });

        var onRefreshPerformance = Util.mkEvent();

        sidebar.addItem('refresh-performance', function(cb){
            var btn = blocks.button('primary', '', Messages.oo_refresh);
            Util.onClickEnter($(btn), function () {
                onRefreshPerformance.fire();
            });
            cb(btn);
        }, {
            noTitle: true,
            noHint: true
        });

        // Msg.admin_performanceProfilingHint, .admin_performanceProfilingTitle
        sidebar.addItem('performance-profiling', function(cb){
            var header = [
                Messages.admin_performanceKeyHeading,
                Messages.admin_performanceTimeHeading,
                Messages.admin_performancePercentHeading
            ];

            var table = blocks.table(header, []);
            table.setAttribute('id', 'cp-admin-table');
            let div = blocks.block([table]);
            div.setAttribute('id', 'cp-admin-table-container');

            const onRefresh = function () {
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'GET_WORKER_PROFILES',
                }, function (e, data) {
                    if (e || data.error) {
                        UI.warn(Messages.error);
                        console.error(e, data);
                        return;
                    }

                    var o = data[0];
                    var sorted = Object.keys(o).sort(function (a, b) {
                        if (o[b] - o[a] <= 0) { return -1; }
                        return 1;
                    });

                    var total = 0;
                    sorted.forEach(function (key) { total += o[key]; });

                    const newRows = sorted.map(function (key) {
                        var percent = Math.floor((o[key] / total) * 1000) / 10;
                        return [key, o[key], percent + '%'];
                    });

                    table.updateContent(newRows);
                });
            };

            onRefresh();
            onRefreshPerformance.reg(onRefresh);

            cb(div);
        });


        // Msg.admin_enableDiskMeasurementsTitle.admin_enableDiskMeasurementsHint
        sidebar.addCheckboxItem({
            getState: function () {
                return APP.instanceStatus.enableProfiling;
            },
            key: 'enable-disk-measurements',
            options: { htmlHint: true },
            query: function (val, setState) {
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'ADMIN_DECREE',
                    data: ['ENABLE_PROFILING', [val]]
                }, function (e, response) {
                    if (e || response.error) {
                        UI.warn(Messages.error);
                        console.error(e, response);
                    }
                    APP.updateStatus(function () {
                        setState(APP.instanceStatus.enableProfiling);
                    });
                });
            }
        });

        var isPositiveInteger = function (n) {
            return n && typeof(n) === 'number'  && n % 1 === 0 && n > 0;
        };

        // Msg.admin_bytesWrittenTitle.admin_bytesWrittenHint
        sidebar.addItem('bytes-written', function(cb){
            var duration = APP.instanceStatus.profilingWindow;
            if (!isPositiveInteger(duration)) { duration = 10000; }
            var newDuration = blocks.input({
                type:'number',
                min: 0,
                value: duration
            });
            var set = blocks.button('primary', '', Messages.admin_setDuration);
            var label = blocks.labelledInput( Messages.ui_ms, newDuration);
            var nav = blocks.nav([set]);
            var form = blocks.form([
                label
            ], nav);
            UI.confirmButton(set, {
                classes: 'btn-primary',
                multiple: true,
                validate: function () {
                    var l = parseInt($(newDuration).val());
                    if (isNaN(l)) {
                        return void UI.warn(Messages.error_limit);
                    }
                    return true;
                }
            }, function () {
                var d = parseInt($(newDuration).val());
                if (!isPositiveInteger(d)) {
                    return void UI.warn(Messages.error_positiveNumber);
                }

                var data = [d];
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'ADMIN_DECREE',
                    data: ['SET_PROFILING_WINDOW', data]
                }, function (e, response) {
                    if (e || response.error) {
                        UI.warn(Messages.error);
                        return void console.error(e, response);
                    }
                    $(form).find('.cp-admin-bytes-written-duration').text(Messages._getKey('admin_bytesWrittenDuration', [d]));
                    UI.log(Messages.saved);
                });
            });
            cb(form);
        });

        // Messages.admin_updateAvailableTitle.admin_updateAvailableHint.admin_updateAvailableLabel.admin_updateAvailableButton
        sidebar.addItem('update-available', function(cb){
            if (!APP.instanceStatus.updateAvailable) { return; }

            var updateURL = 'https://github.com/cryptpad/cryptpad/releases/latest';
            if (typeof(APP.instanceStatus.updateAvailable) === 'string') {
                updateURL = APP.instanceStatus.updateAvailable;
            }
            var button = blocks.button('primary', '', Messages.admin_updateAvailableButton);
            Util.onClickEnter($(button), function () {
                common.openURL(updateURL);
            });

            cb(button);
        });

        // Messages.admin_checkupButton.admin_checkupHint.admin_checkupTitle
        sidebar.addItem('checkup', function(cb){
            var button = blocks.button('primary', '', Messages.admin_checkupButton);
            Util.onClickEnter($(button), function () {
                common.openURL('/checkup/');
            });

            cb(button);
        });


        // Messages.admin_blockDailyCheckTitle.admin_blockDailyCheckHint.admin_blockDailyCheckLabel
        sidebar.addCheckboxItem({
            key: 'block-daily-check',
            getState: function () {
                return  APP.instanceStatus.blockDailyCheck;
            },
            query: function (val, setState) {
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'ADMIN_DECREE',
                    data: ['BLOCK_DAILY_CHECK', [val]]
                }, function (e, response) {
                    if (e || response.error) {
                        UI.warn(Messages.error);
                        console.error(e, response);
                    }
                    APP.updateStatus(function () {
                        setState(APP.instanceStatus.blockDailyCheck);
                    });
                });
            }
        });

        // Messages.admin_provideAggregateStatisticsTitle.admin_provideAggregateStatisticsHint.admin_provideAggregateStatisticsLabel
        sidebar.addCheckboxItem({
            key: 'provide-aggregate-statistics',
            getState: function () {
                return APP.instanceStatus.provideAggregateStatistics;
            },
            query: function (val, setState) {
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'ADMIN_DECREE',
                    data: ['PROVIDE_AGGREGATE_STATISTICS', [val]]
                }, function (e, response) {
                    if (e || response.error) {
                        UI.warn(Messages.error);
                        console.error(e, response);
                    }
                    APP.updateStatus(function () {
                        setState(APP.instanceStatus.provideAggregateStatistics);
                    });
                });
            }
        });

        // Messages.admin_listMyInstanceTitle.admin_listMyInstanceHint.admin_listMyInstanceLabel
        sidebar.addCheckboxItem({
            key: 'list-my-instance',
            getState: function () {
                return APP.instanceStatus.listMyInstance;
            },
            query: function (val, setState) {
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'ADMIN_DECREE',
                    data: ['LIST_MY_INSTANCE', [val]]
                }, function (e, response) {
                    if (e || response.error) {
                        UI.warn(Messages.error);
                        console.error(e, response);
                    }
                    APP.updateStatus(function () {
                        setState(APP.instanceStatus.listMyInstance);
                    });
                });
            }
        });

        // Messages.admin_consentToContactTitle.admin_consentToContactHint.admin_consentToContactLabel
        sidebar.addCheckboxItem({
            key: 'consent-to-contact',
            getState: function () {
                return APP.instanceStatus.consentToContact;
            },
            query: function (val, setState) {
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'ADMIN_DECREE',
                    data: ['CONSENT_TO_CONTACT', [val]]
                }, function (e, response) {
                    if (e || response.error) {
                        UI.warn(Messages.error);
                        console.error(e, response);
                    }
                    APP.updateStatus(function () {
                        setState(APP.instanceStatus.consentToContact);
                    });
                });
            }
        });

        // Messages.admin_removeDonateButtonTitle.admin_removeDonateButtonHint.admin_removeDonateButtonLabel
        sidebar.addCheckboxItem({
            key: 'remove-donate-button',
            getState: function () {
                return APP.instanceStatus.removeDonateButton;
            },
            query: function (val, setState) {
                sFrameChan.query('Q_ADMIN_RPC', {
                    cmd: 'ADMIN_DECREE',
                    data: ['REMOVE_DONATE_BUTTON', [val]]
                }, function (e, response) {
                    if (e || response.error) {
                        UI.warn(Messages.error);
                        console.error(e, response);
                    }
                    APP.updateStatus(function () {
                        setState(APP.instanceStatus.removeDonateButton);
                    });
                });
            }
        });


        var sendDecree = function (data, cb) {
            sFrameChan.query('Q_ADMIN_RPC', {
                cmd: 'ADMIN_DECREE',
                data: data,
            }, cb);
        };

        // Messages.admin_instancePurposeTitle.admin_instancePurposeHint
        sidebar.addItem('instance-purpose', function(cb){
            var values = [
                'noanswer', // Messages.admin_purpose_noanswer
                'experiment', // Messages.admin_purpose_experiment
                'personal', // Messages.admin_purpose_personal
                'education', // Messages.admin_purpose_education
                'org', // Messages.admin_purpose_org
                'business', // Messages.admin_purpose_business
                'public', // Messages.admin_purpose_public
            ];
            var defaultPurpose = 'noanswer';
            var purpose = APP.instanceStatus.instancePurpose || defaultPurpose;

            var opts = values.map(function (key) {
                var full_key = 'admin_purpose_' + key;
                return UI.createRadio('cp-instance-purpose-radio', 'cp-instance-purpose-radio-'+key,
                    Messages[full_key] || Messages._getKey(full_key, [defaultPurpose]),
                    key === purpose, {
                        input: { value: key },
                        //label: { class: 'noTitle' }
                    });
            });

            var $opts = $(opts);

            var setPurpose = function (value, cb) {
                sendDecree([
                    'SET_INSTANCE_PURPOSE',
                    [ value]
                ], cb);
            };

            $opts.on('change', function () {
                var val = $opts.find('input:radio:checked').val();
                console.log(val);
                //spinner.spin();
                setPurpose(val, function (e, response) {
                    if (e || response.error) {
                        UI.warn(Messages.error);
                        //spinner.hide();
                        return;
                    }
                    //spinner.done();
                    UI.log(Messages.saved);
                });
            });

            cb(opts);
        });

        // EXTENSION_POINT:ADMIN_ITEM
        let utils = {
            $, h, Util, Hash, UIElements, UI, APP
        };
        common.getExtensionsSync('ADMIN_ITEM').forEach(ext => {
            if (!ext || !ext.id || typeof(ext.getContent) !== "function") {
                return console.error('Invalid extension point', 'ADMIN_CATEGORY', ext);
            }
            if (sidebar.hasItem(ext.id)) {
                return console.error('Extension point ID already used', ext);
            }
            sidebar.addItem(ext.id, cb => {
                ext.getContent(common, blocks, utils, content => {
                    cb(content);
                });
            }, {
                noTitle: !ext.title,
                noHint: !ext.description,
                title: ext.title,
                hint: ext.description
            });
        });

        sidebar.makeLeftside(categories);
    };
    var updateStatus = APP.updateStatus = function (cb) {
        nThen(w => {
            sFrameChan.query('Q_ADMIN_RPC', {
                cmd: 'INSTANCE_STATUS',
            }, w(function (e, data) {
                if (e) { console.error(e); return void cb(e); }
                if (!Array.isArray(data)) { return void cb('EINVAL'); }
                APP.instanceStatus = data[0];
                console.log("Status", APP.instanceStatus);
            }));
            require([`/api/config?${+new Date()}`], w(ApiConfig => {
                APP.instanceConfig = ApiConfig;
            }));
        }).nThen(() => {
            cb();
        });
    };

    var createToolbar = function () {
        var displayed = ['useradmin', 'newpad', 'limit', 'pageTitle', 'notifications'];
        var configTb = {
            displayed: displayed,
            sfCommon: common,
            $container: APP.$toolbar,
            pageTitle: Messages.adminPage || 'Admin',
            metadataMgr: common.getMetadataMgr(),
            skipLink: '#cp-sidebarlayout-container'
        };
        APP.toolbar = Toolbar.create(configTb);
        APP.toolbar.$rightside.hide();
    };

    nThen(function(waitFor) {
        $(waitFor(UI.addLoadingScreen));
        SFCommon.create(waitFor(function(c) { APP.common = common = c; }));
    }).nThen(function(waitFor) {
        APP.$container = $('#cp-sidebarlayout-container');
        APP.$toolbar = $('#cp-toolbar');
        sFrameChan = common.getSframeChannel();
        sFrameChan.onReady(waitFor());
    }).nThen(function (waitFor) {
        if (!common.isAdmin()) { return; }
        updateStatus(waitFor());
    }).nThen(function( /*waitFor*/ ) {
        common.setTabTitle(Messages.adminPage || 'Administration');

        if (!common.isAdmin()) {
            return void UI.errorLoadingScreen(Messages.admin_authError || '403 Forbidden');
        }

        // Add toolbar
        createToolbar();

        APP.supportModule = common.makeUniversal('support');

        // Content
        andThen(common, APP.$container);

        UI.removeLoadingScreen();
    });
});
