netdata/netdata

View on GitHub
src/web/gui/main.js

Summary

Maintainability
F
1 mo
Test Coverage
// Main JavaScript file for the Netdata GUI.

// Codacy declarations
/* global NETDATA */

// netdata snapshot data
var netdataSnapshotData = null;

// enable alarms checking and notifications
var netdataShowAlarms = true;

// enable registry updates
var netdataRegistry = true;

// forward definition only - not used here
var netdataServer = undefined;
var netdataServerStatic = undefined;
var netdataCheckXSS = undefined;

// control the welcome modal and analytics
var this_is_demo = null;

function escapeUserInputHTML(s) {
    return s.toString()
        .replace(/&/g, '&')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/#/g, '&#35;')
        .replace(/'/g, '&#39;')
        .replace(/\(/g, '&#40;')
        .replace(/\)/g, '&#41;')
        .replace(/\//g, '&#47;');
}

function verifyURL(s) {
    if (typeof (s) === 'string' && (s.startsWith('http://') || s.startsWith('https://'))) {
        return s
            .replace(/'/g, '%22')
            .replace(/"/g, '%27')
            .replace(/\)/g, '%28')
            .replace(/\(/g, '%29');
    }

    console.log('invalid URL detected:');
    console.log(s);
    return 'javascript:alert("invalid url");';
}

// --------------------------------------------------------------------
// urlOptions

var urlOptions = {
    hash: '#',
    theme: null,
    help: null,
    mode: 'live',         // 'live', 'print'
    update_always: false,
    pan_and_zoom: false,
    server: null,
    after: 0,
    before: 0,
    highlight: false,
    highlight_after: 0,
    highlight_before: 0,
    nowelcome: false,
    show_alarms: false,
    chart: null,
    family: null,
    alarm: null,
    alarm_unique_id: 0,
    alarm_id: 0,
    alarm_event_id: 0,
    alarm_when: 0,

    hasProperty: function (property) {
        // console.log('checking property ' + property + ' of type ' + typeof(this[property]));
        return typeof this[property] !== 'undefined';
    },

    genHash: function (forReload) {
        var hash = urlOptions.hash;

        if (urlOptions.pan_and_zoom === true) {
            hash += ';after=' + urlOptions.after.toString() +
                ';before=' + urlOptions.before.toString();
        }

        if (urlOptions.highlight === true) {
            hash += ';highlight_after=' + urlOptions.highlight_after.toString() +
                ';highlight_before=' + urlOptions.highlight_before.toString();
        }

        if (urlOptions.theme !== null) {
            hash += ';theme=' + urlOptions.theme.toString();
        }

        if (urlOptions.help !== null) {
            hash += ';help=' + urlOptions.help.toString();
        }

        if (urlOptions.update_always === true) {
            hash += ';update_always=true';
        }

        if (forReload === true && urlOptions.server !== null) {
            hash += ';server=' + urlOptions.server.toString();
        }

        if (urlOptions.mode !== 'live') {
            hash += ';mode=' + urlOptions.mode;
        }

        return hash;
    },

    parseHash: function () {
        var variables = document.location.hash.split(';');
        var len = variables.length;
        while (len--) {
            if (len !== 0) {
                var p = variables[len].split('=');
                if (urlOptions.hasProperty(p[0]) && typeof p[1] !== 'undefined') {
                    urlOptions[p[0]] = decodeURIComponent(p[1]);
                }
            } else {
                if (variables[len].length > 0) {
                    urlOptions.hash = variables[len];
                }
            }
        }

        var booleans = ['nowelcome', 'show_alarms', 'update_always'];
        len = booleans.length;
        while (len--) {
            if (urlOptions[booleans[len]] === 'true' || urlOptions[booleans[len]] === true || urlOptions[booleans[len]] === '1' || urlOptions[booleans[len]] === 1) {
                urlOptions[booleans[len]] = true;
            } else {
                urlOptions[booleans[len]] = false;
            }
        }

        var numeric = ['after', 'before', 'highlight_after', 'highlight_before', 'alarm_when'];
        len = numeric.length;
        while (len--) {
            if (typeof urlOptions[numeric[len]] === 'string') {
                try {
                    urlOptions[numeric[len]] = parseInt(urlOptions[numeric[len]]);
                }
                catch (e) {
                    console.log('failed to parse URL hash parameter ' + numeric[len]);
                    urlOptions[numeric[len]] = 0;
                }
            }
        }

        if (urlOptions.alarm_when) {
            // if alarm_when exists, create after/before params
            // -/+ 2 minutes from the alarm, and reload the page
            const alarmTime = new Date(urlOptions.alarm_when * 1000).valueOf();
            const timeMarginMs = 120000; // 2 mins

            const after = alarmTime - timeMarginMs;
            const before = alarmTime + timeMarginMs;
            const newHash = document.location.hash.replace(
                /;alarm_when=[0-9]*/i,
                ";after=" + after + ";before=" + before,
            );
            history.replaceState(null, '', newHash);
            location.reload();
        }

        if (urlOptions.server !== null && urlOptions.server !== '') {
            netdataServerStatic = document.location.origin.toString() + document.location.pathname.toString();
            netdataServer = urlOptions.server;
            netdataCheckXSS = true;
        } else {
            urlOptions.server = null;
        }

        if (urlOptions.before > 0 && urlOptions.after > 0) {
            urlOptions.pan_and_zoom = true;
            urlOptions.nowelcome = true;
        } else {
            urlOptions.pan_and_zoom = false;
        }

        if (urlOptions.highlight_before > 0 && urlOptions.highlight_after > 0) {
            urlOptions.highlight = true;
        } else {
            urlOptions.highlight = false;
        }

        switch (urlOptions.mode) {
            case 'print':
                urlOptions.theme = 'white';
                urlOptions.welcome = false;
                urlOptions.help = false;
                urlOptions.show_alarms = false;

                if (urlOptions.pan_and_zoom === false) {
                    urlOptions.pan_and_zoom = true;
                    urlOptions.before = Date.now();
                    urlOptions.after = urlOptions.before - 600000;
                }

                netdataShowAlarms = false;
                netdataRegistry = false;
                this_is_demo = false;
                break;

            case 'live':
            default:
                urlOptions.mode = 'live';
                break;
        }

        // console.log(urlOptions);
    },

    hashUpdate: function () {
        history.replaceState(null, '', urlOptions.genHash(true));
    },

    netdataPanAndZoomCallback: function (status, after, before) {
        //console.log(1);
        //console.log(new Error().stack);

        if (netdataSnapshotData === null) {
            urlOptions.pan_and_zoom = status;
            urlOptions.after = after;
            urlOptions.before = before;
            urlOptions.hashUpdate();
        }
    },

    netdataHighlightCallback: function (status, after, before) {
        //console.log(2);
        //console.log(new Error().stack);

        if (status === true && (after === null || before === null || after <= 0 || before <= 0 || after >= before)) {
            status = false;
            after = 0;
            before = 0;
        }

        if (netdataSnapshotData === null) {
            urlOptions.highlight = status;
        } else {
            urlOptions.highlight = false;
        }

        urlOptions.highlight_after = Math.round(after);
        urlOptions.highlight_before = Math.round(before);
        urlOptions.hashUpdate();

        var show_eye = NETDATA.globalChartUnderlay.hasViewport();

        if (status === true && after > 0 && before > 0 && after < before) {
            var d1 = NETDATA.dateTime.localeDateString(after);
            var d2 = NETDATA.dateTime.localeDateString(before);
            if (d1 === d2) {
                d2 = '';
            }
            document.getElementById('navbar-highlight-content').innerHTML =
                ((show_eye === true) ? '<span class="navbar-highlight-bar highlight-tooltip" onclick="urlOptions.showHighlight();" title="restore the highlighted view" data-toggle="tooltip" data-placement="bottom">' : '<span>').toString()
                + 'highlighted time-frame'
                + ' <b>' + d1 + ' <code>' + NETDATA.dateTime.localeTimeString(after) + '</code></b> to '
                + ' <b>' + d2 + ' <code>' + NETDATA.dateTime.localeTimeString(before) + '</code></b>, '
                + 'duration <b>' + NETDATA.seconds4human(Math.round((before - after) / 1000)) + '</b>'
                + '</span>'
                + '<span class="navbar-highlight-button-right highlight-tooltip" onclick="urlOptions.clearHighlight();" title="clear the highlighted time-frame" data-toggle="tooltip" data-placement="bottom"><i class="fas fa-times"></i></span>';

            $('.navbar-highlight').show();

            $('.highlight-tooltip').tooltip({
                html: true,
                delay: { show: 500, hide: 0 },
                container: 'body'
            });
        } else {
            $('.navbar-highlight').hide();
        }
    },

    clearHighlight: function () {
        NETDATA.globalChartUnderlay.clear();

        if (NETDATA.globalPanAndZoom.isActive() === true) {
            NETDATA.globalPanAndZoom.clearMaster();
        }
    },

    showHighlight: function () {
        NETDATA.globalChartUnderlay.focus();
    }
};

urlOptions.parseHash();

// --------------------------------------------------------------------
// check options that should be processed before loading netdata.js

var localStorageTested = -1;

function localStorageTest() {
    if (localStorageTested !== -1) {
        return localStorageTested;
    }

    if (typeof Storage !== "undefined" && typeof localStorage === 'object') {
        var test = 'test';
        try {
            localStorage.setItem(test, test);
            localStorage.removeItem(test);
            localStorageTested = true;
        }
        catch (e) {
            console.log(e);
            localStorageTested = false;
        }
    } else {
        localStorageTested = false;
    }

    return localStorageTested;
}

function loadLocalStorage(name) {
    var ret = null;

    try {
        if (localStorageTest() === true) {
            ret = localStorage.getItem(name);
        } else {
            console.log('localStorage is not available');
        }
    }
    catch (error) {
        console.log(error);
        return null;
    }

    if (typeof ret === 'undefined' || ret === null) {
        return null;
    }

    // console.log('loaded: ' + name.toString() + ' = ' + ret.toString());

    return ret;
}

function saveLocalStorage(name, value) {
    // console.log('saving: ' + name.toString() + ' = ' + value.toString());
    try {
        if (localStorageTest() === true) {
            localStorage.setItem(name, value.toString());
            return true;
        }
    }
    catch (error) {
        console.log(error);
    }

    return false;
}

function getTheme(def) {
    if (urlOptions.mode === 'print') {
        return 'white';
    }

    var ret = loadLocalStorage('netdataTheme');
    if (typeof ret === 'undefined' || ret === null || ret === 'undefined') {
        return def;
    } else {
        return ret;
    }
}

function setTheme(theme) {
    if (urlOptions.mode === 'print') {
        return false;
    }

    if (theme === netdataTheme) {
        return false;
    }
    return saveLocalStorage('netdataTheme', theme);
}

var netdataTheme = getTheme('slate');
var netdataShowHelp = true;

if (urlOptions.theme !== null) {
    setTheme(urlOptions.theme);
    netdataTheme = urlOptions.theme;
} else {
    urlOptions.theme = netdataTheme;
}

if (urlOptions.help !== null) {
    saveLocalStorage('options.show_help', urlOptions.help);
    netdataShowHelp = urlOptions.help;
} else {
    urlOptions.help = loadLocalStorage('options.show_help');
}

// --------------------------------------------------------------------
// natural sorting
// http://www.davekoelle.com/files/alphanum.js - LGPL

function naturalSortChunkify(t) {
    var tz = [];
    var x = 0, y = -1, n = 0, i, j;

    while (i = (j = t.charAt(x++)).charCodeAt(0)) {
        var m = (i >= 48 && i <= 57);
        if (m !== n) {
            tz[++y] = "";
            n = m;
        }
        tz[y] += j;
    }

    return tz;
}

function naturalSortCompare(a, b) {
    var aa = naturalSortChunkify(a.toLowerCase());
    var bb = naturalSortChunkify(b.toLowerCase());

    for (var x = 0; aa[x] && bb[x]; x++) {
        if (aa[x] !== bb[x]) {
            var c = Number(aa[x]), d = Number(bb[x]);
            if (c.toString() === aa[x] && d.toString() === bb[x]) {
                return c - d;
            } else {
                return (aa[x] > bb[x]) ? 1 : -1;
            }
        }
    }

    return aa.length - bb.length;
}

// --------------------------------------------------------------------
// saving files to client

function saveTextToClient(data, filename) {
    var blob = new Blob([data], {
        type: 'application/octet-stream'
    });

    var url = URL.createObjectURL(blob);
    var link = document.createElement('a');
    link.setAttribute('href', url);
    link.setAttribute('download', filename);

    var el = document.getElementById('hiddenDownloadLinks');
    el.innerHTML = '';
    el.appendChild(link);

    setTimeout(function () {
        el.removeChild(link);
        URL.revokeObjectURL(url);
    }, 60);

    link.click();
}

function saveObjectToClient(data, filename) {
    saveTextToClient(JSON.stringify(data), filename);
}

// -----------------------------------------------------------------------------
// registry call back to render my-netdata menu

function toggleExpandIcon(svgEl) {
    if (svgEl.getAttribute('data-icon') === 'caret-down') {
        svgEl.setAttribute('data-icon', 'caret-up');
    } else {
        svgEl.setAttribute('data-icon', 'caret-down');
    }
}

function toggleAgentItem(e, guid) {
    e.stopPropagation();
    e.preventDefault();

    toggleExpandIcon(e.currentTarget.children[0]);

    const el = document.querySelector(`.agent-alternate-urls.agent-${guid}`);
    if (el) {
        el.classList.toggle('collapsed');
    }
}

// When you stream metrics from netdata to netdata, the receiving netdata now
// has multiple host databases. It's own, and multiple mirrored. Mirrored databases
// can be accessed with <http://localhost:19999/host/NAME/>
const OLD_DASHBOARD_SUFFIX = "old"
let isOldSuffix = true
try {
    const currentScriptMainJs = document.currentScript;
    const mainJsSrc = currentScriptMainJs.getAttribute("src")
    isOldSuffix = mainJsSrc.startsWith("../main.js")
} catch {
    console.warn("current script not detecting, assuming the dashboard is running with /old suffix")
}

function transformWithOldSuffix(url) {
    return isOldSuffix ? `../${url}` : url
}

function renderStreamedHosts(options) {
    let html = `<div class="info-item">Databases streamed to this agent</div>`;

    var base = document.location.origin.toString() +
      document.location.pathname.toString()
        .replace(isOldSuffix ? `/${OLD_DASHBOARD_SUFFIX}` : "", "");
    if (base.endsWith("/host/" + options.hostname + "/")) {
        base = base.substring(0, base.length - ("/host/" + options.hostname + "/").toString().length);
    }

    if (base.endsWith("/")) {
        base = base.substring(0, base.length - 1);
    }

    var master = options.hosts[0].hostname;
    // We sort a clone of options.hosts, to keep the master as the first element
    // for future calls.
    var sorted = options.hosts.slice(0).sort(function (a, b) {
        if (a.hostname === master) {
            return -1;
        }
        return naturalSortCompare(a.hostname, b.hostname);
    });

    let displayedDatabases = false;

    for (var s of sorted) {
        let url, icon;
        const hostname = s.hostname;

        if (myNetdataMenuFilterValue !== "") {
            if (!hostname.includes(myNetdataMenuFilterValue)) {
                continue;
            }
        }

        displayedDatabases = true;

        if (hostname === master) {
            url = isOldSuffix ? `${base}/${OLD_DASHBOARD_SUFFIX}/` : `${base}/`;
            icon = 'home';
        } else {
            url = isOldSuffix ? `${base}/host/${hostname}/${OLD_DASHBOARD_SUFFIX}/` : `${base}/host/${hostname}/`;
            icon = 'window-restore';
        }

        html += (
            `<div class="agent-item">
                <a class="registry_link" href="${url}#" onClick="return gotoHostedModalHandler('${url}');">
                    <i class="fas fa-${icon}" style="color: #999;"></i>
                </a>
                <span class="__title" onClick="return gotoHostedModalHandler('${url}');">
                    <a class="registry_link" href="${url}#">${hostname}</a>
                </span>
                <div></div>
            </div>`
        )
    }

    if (!displayedDatabases) {
        html += (
            `<div class="info-item">
                <i class="fas fa-filter"></i>
                <span style="margin-left: 8px">no databases match the filter criteria.<span>
            </div>`
        )
    }

    return html;
}

function renderMachines(machinesArray) {
    let html = `<div class="info-item">My nodes</div>`;

    if (machinesArray === null) {
        let ret = loadLocalStorage("registryCallback");
        if (ret) {
            machinesArray = JSON.parse(ret);
            console.log("failed to contact the registry - loaded registry data from browser local storage");
        }
    }

    let found = false;
    let displayedAgents = false;

    const maskedURL = NETDATA.registry.MASKED_DATA;

    if (machinesArray) {
        saveLocalStorage("registryCallback", JSON.stringify(machinesArray));

        var machines = machinesArray.sort(function (a, b) {
            return naturalSortCompare(a.name, b.name);
        });

        for (var machine of machines) {
            found = true;

            if (myNetdataMenuFilterValue !== "") {
                if (!machine.name.includes(myNetdataMenuFilterValue)) {
                    continue;
                }
            }

            displayedAgents = true;

            const alternateUrlItems = (
                `<div class="agent-alternate-urls agent-${machine.guid} collapsed">
                ${machine.alternate_urls.reduce((str, url) => {
                    if (url === maskedURL) {
                        return str
                    }

                    return str + (
                        `<div class="agent-item agent-item--alternate">
                                <div></div>
                                <a href="${url}" title="${url}">${truncateString(url, 64)}</a>
                                <a href="#" onclick="deleteRegistryModalHandler('${machine.guid}', '${machine.name}', '${url}'); return false;">
                                    <i class="fas fa-trash" style="color: #777;"></i>
                                </a>
                            </div>`
                    )
                },
                    ''
                )}
                </div>`
            )

            html += (
                `<div class="agent-item agent-${machine.guid}">
                    <i class="fas fa-chart-bar" color: #fff"></i>
                    <span class="__title" onClick="return gotoServerModalHandler('${machine.guid}');">
                        <a class="registry_link" href="${machine.url}#">${machine.name}</a>
                    </span>
                    <a href="#" onClick="toggleAgentItem(event, '${machine.guid}');">
                        <i class="fas fa-caret-down" style="color: #999"></i>
                    </a>
                </div>
                ${alternateUrlItems}`
            )
        }

        if (found && (!displayedAgents)) {
            html += (
                `<div class="info-item">
                    <i class="fas fa-filter"></i>
                    <span style="margin-left: 8px">zero nodes are matching the filter value.<span>
                </div>`
            )
        }
    }

    if (!found) {
        if (machines) {
            html += (
                `<div class="info-item">
                    <a href="https://github.com/netdata/netdata/tree/master/src/registry#registry" target="_blank">Your nodes list is empty</a>
                </div>`
            )
        } else {
            html += (
                `<div class="info-item">
                    <a href="https://github.com/netdata/netdata/tree/master/src/registry#registry" target="_blank">Failed to contact the registry</a>
                </div>`
            )
        }

        html += `<hr />`;
        html += `<div class="info-item">Demo netdata nodes</div>`;

        const demoServers = [
            { url: "//london.netdata.rocks/default.html", title: "UK - London (DigitalOcean.com)" },
            { url: "//newyork.netdata.rocks/default.html", title: "US - New York (DigitalOcean.com)" },
            { url: "//sanfrancisco.netdata.rocks/default.html", title: "US - San Francisco (DigitalOcean.com)" },
            { url: "//atlanta.netdata.rocks/default.html", title: "US - Atlanta (CDN77.com)" },
            { url: "//frankfurt.netdata.rocks/default.html", title: "Germany - Frankfurt (DigitalOcean.com)" },
            { url: "//toronto.netdata.rocks/default.html", title: "Canada - Toronto (DigitalOcean.com)" },
            { url: "//singapore.netdata.rocks/default.html", title: "Japan - Singapore (DigitalOcean.com)" },
            { url: "//bangalore.netdata.rocks/default.html", title: "India - Bangalore (DigitalOcean.com)" },

        ]

        for (var server of demoServers) {
            html += (
                `<div class="agent-item">
                    <i class="fas fa-chart-bar" style="color: #fff"></i>
                    <a href="${server.url}">${server.title}</a>
                    <div></div>
                </div>
                `
            );
        }
    }

    return html;
}

function setMyNetdataMenu(html) {
    const el = document.getElementById('my-netdata-dropdown-content')
    el.innerHTML = html;
}

function clearMyNetdataMenu() {
    setMyNetdataMenu(`<div class="agent-item" style="white-space: nowrap">
        <i class="fas fa-hourglass-half"></i>
        Loading, please wait...
        <div></div>
    </div>`);
}

function errorMyNetdataMenu() {
    setMyNetdataMenu(`<div class="agent-item" style="padding: 0 8px">
        <i class="fas fa-exclamation-triangle" style="color: red"></i>
        Cannot load known Netdata agents from Netdata Cloud! Please make sure you have the latest version of Netdata.
    </div>`);
}

function restrictMyNetdataMenu() {
    setMyNetdataMenu(`<div class="info-item" style="white-space: nowrap">
        <span>Please <a href="#" onclick="signInDidClick(event); return false">sign in to netdata.cloud</a> to view your nodes!</span>
        <div></div>
    </div>`);
}

function openAuthenticatedUrl(url) {
    if (isSignedIn()) {
        window.open(url);
    } else {
        window.open(`${NETDATA.registry.cloudBaseURL}/account/sign-in-agent?id=${NETDATA.registry.machine_guid}&name=${encodeURIComponent(NETDATA.registry.hostname)}&origin=${encodeURIComponent(window.location.origin + "/")}&redirect_uri=${encodeURIComponent(window.location.origin + "/" + url)}`);
    }
}

function renderMyNetdataMenu(machinesArray) {
    const el = document.getElementById('my-netdata-dropdown-content');
    el.classList.add(`theme-${netdataTheme}`);

    if (machinesArray == registryAgents) {
        console.log("Rendering my-netdata menu from registry");
    } else {
        console.log("Rendering my-netdata menu from netdata.cloud", machinesArray);
    }

    let html = '';

    if (!isSignedIn()) {
        if (!NETDATA.registry.isRegistryEnabled()) {
            html += (
                `<div class="info-item" style="white-space: nowrap">
                    <span>Please <a href="#" onclick="signInDidClick(event); return false">sign in to netdata.cloud</a> to view your nodes!</span>
                    <div></div>
                </div>
                <hr />`
            );
        }
    }

    if (isSignedIn()) {
        html += (
            `<div class="filter-control">
                <input 
                    id="my-netdata-menu-filter-input"
                    type="text" 
                    placeholder="filter nodes..."
                    autofocus
                    autocomplete="off"
                    value="${myNetdataMenuFilterValue}" 
                    onkeydown="myNetdataFilterDidChange(event)"
                />
                <span class="filter-control__clear" onclick="myNetdataFilterClearDidClick(event)"><i class="fas fa-times"></i><span>
            </div>
            <hr />`
        );
    }

    // options.hosts = [
    //     {
    //         hostname: "streamed1",
    //     },
    //     {
    //         hostname: "streamed2",
    //     },
    // ]

    if (options.hosts.length > 1) {
        html += `<div id="my-netdata-menu-streamed">${renderStreamedHosts(options)}</div><hr />`;
    }

    if (isSignedIn() || NETDATA.registry.isRegistryEnabled()) {
        html += `<div id="my-netdata-menu-machines">${renderMachines(machinesArray)}</div><hr />`;
    }

    if (!isSignedIn()) {
        html += (
            `<div class="agent-item">
                <i class="fas fa-cog""></i>
                <a href="#" onclick="switchRegistryModalHandler(); return false;">Switch Identity</a>
                <div></div>
            </div>
            <div class="agent-item">
                <i class="fas fa-question-circle""></i>
                <a href="https://github.com/netdata/netdata/tree/master/src/registry#registry" target="_blank">What is this?</a>
                <div></div>
            </div>`
        )
    } else {
        html += (
            `<div class="agent-item">
                <i class="fas fa-tv"></i>
                <a onclick="openAuthenticatedUrl('console.html');" target="_blank">Nodes<sup class="beta"> beta</sup></a>
                <div></div>
            </div>
            <div class="agent-item">
                <i class="fas fa-sync"></i>
                <a href="#" onclick="showSyncModal(); return false">Synchronize with netdata.cloud</a>
                <div></div>
            </div>
            <div class="agent-item">
                <i class="fas fa-question-circle""></i>
                <a href="https://netdata.cloud/about" target="_blank">What is this?</a>
                <div></div>
            </div>`
        )
    }

    el.innerHTML = html;

    gotoServerInit();
}

function isdemo() {
    if (this_is_demo !== null) {
        return this_is_demo;
    }
    this_is_demo = false;

    try {
        if (typeof document.location.hostname === 'string') {
            if (document.location.hostname.endsWith('.my-netdata.io') ||
                document.location.hostname.endsWith('.mynetdata.io') ||
                document.location.hostname.endsWith('.netdata.rocks') ||
                document.location.hostname.endsWith('.netdata.ai') ||
                document.location.hostname.endsWith('.netdata.live') ||
                document.location.hostname.endsWith('.firehol.org') ||
                document.location.hostname.endsWith('.netdata.online') ||
                document.location.hostname.endsWith('.netdata.cloud')) {
                this_is_demo = true;
            }
        }
    }
    catch (error) {
    }
    return this_is_demo;
}

function netdataURL(url, forReload) {
    if (typeof url === 'undefined')
    // url = document.location.toString();
    {
        url = '';
    }

    if (url.indexOf('#') !== -1) {
        url = url.substring(0, url.indexOf('#'));
    }

    var hash = urlOptions.genHash(forReload);

    // console.log('netdataURL: ' + url + hash);

    return url + hash;
}

function netdataReload(url) {
    document.location = verifyURL(netdataURL(url, true));

    // since we play with hash
    // this is needed to reload the page
    location.reload();
}

function gotoHostedModalHandler(url) {
    document.location = verifyURL(url + urlOptions.genHash());
    return false;
}

var gotoServerValidateRemaining = 0;
var gotoServerMiddleClick = false;
var gotoServerStop = false;

function gotoServerValidateUrl(id, guid, url) {
    var penalty = 0;
    var error = 'failed';

    if (document.location.toString().startsWith('http://') && url.toString().startsWith('https://'))
    // we penalize https only if the current url is http
    // to allow the user walk through all its servers.
    {
        penalty = 500;
    } else if (document.location.toString().startsWith('https://') && url.toString().startsWith('http://')) {
        error = 'can\'t check';
    }

    var finalURL = netdataURL(url);

    setTimeout(function () {
        document.getElementById('gotoServerList').innerHTML += '<tr><td style="padding-left: 20px;"><a href="' + verifyURL(finalURL) + '" target="_blank">' + escapeUserInputHTML(url) + '</a></td><td style="padding-left: 30px;"><code id="' + guid + '-' + id + '-status">checking...</code></td></tr>';

        NETDATA.registry.hello(url, function (data) {
            if (typeof data !== 'undefined' && data !== null && typeof data.machine_guid === 'string' && data.machine_guid === guid) {
                // console.log('OK ' + id + ' URL: ' + url);
                document.getElementById(guid + '-' + id + '-status').innerHTML = "OK";

                if (!gotoServerStop) {
                    gotoServerStop = true;

                    if (gotoServerMiddleClick) {
                        window.open(verifyURL(finalURL), '_blank');
                        gotoServerMiddleClick = false;
                        document.getElementById('gotoServerResponse').innerHTML = '<b>Opening new window to ' + NETDATA.registry.machines[guid].name + '<br/><a href="' + verifyURL(finalURL) + '">' + escapeUserInputHTML(url) + '</a></b><br/>(check your pop-up blocker if it fails)';
                    } else {
                        document.getElementById('gotoServerResponse').innerHTML += 'found it! It is at:<br/><small>' + escapeUserInputHTML(url) + '</small>';
                        document.location = verifyURL(finalURL);
                        $('#gotoServerModal').modal('hide');
                    }
                }
            } else {
                if (typeof data !== 'undefined' && data !== null && typeof data.machine_guid === 'string' && data.machine_guid !== guid) {
                    error = 'wrong machine';
                }

                document.getElementById(guid + '-' + id + '-status').innerHTML = error;
                gotoServerValidateRemaining--;
                if (gotoServerValidateRemaining <= 0) {
                    gotoServerMiddleClick = false;
                    document.getElementById('gotoServerResponse').innerHTML = '<b>Sorry! I cannot find any operational URL for this server</b>';
                }
            }
        });
    }, (id * 50) + penalty);
}

function gotoServerModalHandler(guid) {
    // console.log('goto server: ' + guid);

    gotoServerStop = false;
    var checked = {};
    var len = NETDATA.registry.machines[guid].alternate_urls.length;
    var count = 0;

    document.getElementById('gotoServerResponse').innerHTML = '';
    document.getElementById('gotoServerList').innerHTML = '';
    document.getElementById('gotoServerName').innerHTML = NETDATA.registry.machines[guid].name;
    $('#gotoServerModal').modal('show');

    gotoServerValidateRemaining = len;
    while (len--) {
        var url = NETDATA.registry.machines[guid].alternate_urls[len];
        checked[url] = true;
        gotoServerValidateUrl(count++, guid, url);
    }

    if (!isSignedIn()) {
        // When the registry is enabled, if the user's known URLs are not working
        // we consult the registry to get additional URLs.
        setTimeout(function () {
            if (gotoServerStop === false) {
                document.getElementById('gotoServerResponse').innerHTML = '<b>Added all the known URLs for this machine.</b>';
                NETDATA.registry.search(guid, function (data) {
                    // console.log(data);
                    len = data.urls.length;
                    while (len--) {
                        var url = data.urls[len][1];
                        // console.log(url);
                        if (typeof checked[url] === 'undefined') {
                            gotoServerValidateRemaining++;
                            checked[url] = true;
                            gotoServerValidateUrl(count++, guid, url);
                        }
                    }
                });
            }
        }, 2000);
    }

    return false;
}

function gotoServerInit() {
    $(".registry_link").on('click', function (e) {
        if (e.which === 2) {
            e.preventDefault();
            gotoServerMiddleClick = true;
        } else {
            gotoServerMiddleClick = false;
        }

        return true;
    });
}

function switchRegistryModalHandler() {
    document.getElementById('switchRegistryPersonGUID').value = NETDATA.registry.person_guid;
    document.getElementById('switchRegistryURL').innerHTML = NETDATA.registry.server;
    document.getElementById('switchRegistryResponse').innerHTML = '';
    $('#switchRegistryModal').modal('show');
}

function notifyForSwitchRegistry() {
    var n = document.getElementById('switchRegistryPersonGUID').value;

    if (n !== '' && n.length === 36) {
        NETDATA.registry.switch(n, function (result) {
            if (result !== null) {
                $('#switchRegistryModal').modal('hide');
                NETDATA.registry.init();
            } else {
                document.getElementById('switchRegistryResponse').innerHTML = "<b>Sorry! The registry rejected your request.</b>";
            }
        });
    } else {
        document.getElementById('switchRegistryResponse').innerHTML = "<b>The ID you have entered is not a GUID.</b>";
    }
}

var deleteRegistryGuid = null;
var deleteRegistryUrl = null;

function deleteRegistryModalHandler(guid, name, url) {
    // void (guid);

    deleteRegistryGuid = guid;
    deleteRegistryUrl = url;

    document.getElementById('deleteRegistryServerName').innerHTML = name;
    document.getElementById('deleteRegistryServerName2').innerHTML = name;
    document.getElementById('deleteRegistryServerURL').innerHTML = url;
    document.getElementById('deleteRegistryResponse').innerHTML = '';

    $('#deleteRegistryModal').modal('show');
}

function notifyForDeleteRegistry() {
    const responseEl = document.getElementById('deleteRegistryResponse');

    if (deleteRegistryUrl) {
        if (isSignedIn()) {
            deleteCloudAgentURL(deleteRegistryGuid, deleteRegistryUrl)
                .then((count) => {
                    if (!count) {
                        responseEl.innerHTML = "<b>Sorry, this command was rejected by netdata.cloud!</b>";
                        return;
                    }
                    NETDATA.registry.delete(deleteRegistryUrl, function (result) {
                        if (result === null) {
                            console.log("Received error from registry", result);
                        }

                        deleteRegistryUrl = null;
                        $('#deleteRegistryModal').modal('hide');
                        NETDATA.registry.init();
                    });
                });
        } else {
            NETDATA.registry.delete(deleteRegistryUrl, function (result) {
                if (result !== null) {
                    deleteRegistryUrl = null;
                    $('#deleteRegistryModal').modal('hide');
                    NETDATA.registry.init();
                } else {
                    responseEl.innerHTML = "<b>Sorry, this command was rejected by the registry server!</b>";
                }
            });
        }
    }
}

var options = {
    menus: {},
    submenu_names: {},
    data: null,
    hostname: 'netdata_server', // will be overwritten by the netdata server
    version: 'unknown',
    release_channel: 'unknown',
    hosts: [],

    duration: 0, // the default duration of the charts
    update_every: 1,

    chartsPerRow: 0,
    // chartsMinWidth: 1450,
    chartsHeight: 180,
};

function chartsPerRow(total) {
    void (total);

    if (options.chartsPerRow === 0) {
        return 1;
        //var width = Math.floor(total / options.chartsMinWidth);
        //if(width === 0) width = 1;
        //return width;
    } else {
        return options.chartsPerRow;
    }
}

function prioritySort(a, b) {
    if (a.priority < b.priority) {
        return -1;
    }
    if (a.priority > b.priority) {
        return 1;
    }
    return naturalSortCompare(a.name, b.name);
}

function sortObjectByPriority(object) {
    var idx = {};
    var sorted = [];

    for (var i in object) {
        if (!object.hasOwnProperty(i)) {
            continue;
        }

        if (typeof idx[i] === 'undefined') {
            idx[i] = object[i];
            sorted.push(i);
        }
    }

    sorted.sort(function (a, b) {
        if (idx[a].priority < idx[b].priority) {
            return -1;
        }
        if (idx[a].priority > idx[b].priority) {
            return 1;
        }
        return naturalSortCompare(a, b);
    });

    return sorted;
}

// ----------------------------------------------------------------------------
// scroll to a section, without changing the browser history

function scrollToId(hash) {
    if (hash && hash !== '' && document.getElementById(hash) !== null) {
        var offset = $('#' + hash).offset();
        if (typeof offset !== 'undefined') {
            //console.log('scrolling to ' + hash + ' at ' + offset.top.toString());
            $('html, body').animate({ scrollTop: offset.top - 30 }, 0);
        }
    }

    // we must return false to prevent the default action
    return false;
}

// ----------------------------------------------------------------------------

// user editable information
var customDashboard = {
    menu: {},
    submenu: {},
    context: {}
};

// netdata standard information
var netdataDashboard = {
    sparklines_registry: {},
    os: 'unknown',

    menu: {},
    submenu: {},
    context: {},

    // generate a sparkline
    // used in the documentation
    sparkline: function (prefix, chart, dimension, units, suffix) {
        if (options.data === null || typeof options.data.charts === 'undefined') {
            return '';
        }

        if (typeof options.data.charts[chart] === 'undefined') {
            return '';
        }

        if (typeof options.data.charts[chart].dimensions === 'undefined') {
            return '';
        }

        if (typeof options.data.charts[chart].dimensions[dimension] === 'undefined') {
            return '';
        }

        var key = chart + '.' + dimension;

        if (typeof units === 'undefined') {
            units = '';
        }

        if (typeof this.sparklines_registry[key] === 'undefined') {
            this.sparklines_registry[key] = { count: 1 };
        } else {
            this.sparklines_registry[key].count++;
        }

        key = key + '.' + this.sparklines_registry[key].count;

        return prefix + '<div class="netdata-container" data-netdata="' + chart + '" data-after="-120" data-width="25%" data-height="15px" data-chart-library="dygraph" data-dygraph-theme="sparkline" data-dimensions="' + dimension + '" data-show-value-of-' + dimension + '-at="' + key + '"></div> (<span id="' + key + '" style="display: inline-block; min-width: 50px; text-align: right;">X</span>' + units + ')' + suffix;
    },

    gaugeChart: function (title, width, dimensions, colors) {
        if (typeof colors === 'undefined') {
            colors = '';
        }

        if (typeof dimensions === 'undefined') {
            dimensions = '';
        }

        return '<div class="netdata-container" data-netdata="CHART_UNIQUE_ID"'
            + ' data-dimensions="' + dimensions + '"'
            + ' data-chart-library="gauge"'
            + ' data-gauge-adjust="width"'
            + ' data-title="' + title + '"'
            + ' data-width="' + width + '"'
            + ' data-before="0"'
            + ' data-after="-CHART_DURATION"'
            + ' data-points="CHART_DURATION"'
            + ' data-colors="' + colors + '"'
            + ' role="application"></div>';
    },

    anyAttribute: function (obj, attr, key, def) {
        if (typeof (obj[key]) !== 'undefined') {
            var x = obj[key][attr];

            if (typeof (x) === 'undefined') {
                return def;
            }

            if (typeof (x) === 'function') {
                return x(netdataDashboard.os);
            }

            return x;
        }

        return def;
    },

    menuTitle: function (chart) {
        if (typeof chart.menu_pattern !== 'undefined') {
            return (this.anyAttribute(this.menu, 'title', chart.menu_pattern, chart.menu_pattern).toString()
                + '&nbsp;' + chart.type.slice(-(chart.type.length - chart.menu_pattern.length - 1)).toString()).replace(/_/g, ' ');
        }

        return (this.anyAttribute(this.menu, 'title', chart.menu, chart.menu)).toString().replace(/_/g, ' ');
    },

    menuIcon: function (chart) {
        if (typeof chart.menu_pattern !== 'undefined') {
            return this.anyAttribute(this.menu, 'icon', chart.menu_pattern, '<i class="fas fa-puzzle-piece"></i>').toString();
        }

        return this.anyAttribute(this.menu, 'icon', chart.menu, '<i class="fas fa-puzzle-piece"></i>');
    },

    menuInfo: function (chart) {
        if (typeof chart.menu_pattern !== 'undefined') {
            return this.anyAttribute(this.menu, 'info', chart.menu_pattern, null);
        }

        return this.anyAttribute(this.menu, 'info', chart.menu, null);
    },

    menuHeight: function (chart) {
        if (typeof chart.menu_pattern !== 'undefined') {
            return this.anyAttribute(this.menu, 'height', chart.menu_pattern, 1.0);
        }

        return this.anyAttribute(this.menu, 'height', chart.menu, 1.0);
    },

    submenuTitle: function (menu, submenu) {
        var key = menu + '.' + submenu;
        // console.log(key);
        var title = this.anyAttribute(this.submenu, 'title', key, submenu).toString().replace(/_/g, ' ');
        if (title.length > 28) {
            var a = title.substring(0, 13);
            var b = title.substring(title.length - 12, title.length);
            return a + '...' + b;
        }
        return title;
    },

    submenuInfo: function (menu, submenu) {
        var key = menu + '.' + submenu;
        return this.anyAttribute(this.submenu, 'info', key, null);
    },

    submenuHeight: function (menu, submenu, relative) {
        var key = menu + '.' + submenu;
        return this.anyAttribute(this.submenu, 'height', key, 1.0) * relative;
    },

    contextInfo: function (id) {
        var x = this.anyAttribute(this.context, 'info', id, null);

        if (x !== null) {
            return '<div class="shorten dashboard-context-info netdata-chart-alignment" role="document">' + x + '</div>';
        } else {
            return '';
        }
    },

    contextValueRange: function (id) {
        if (typeof this.context[id] !== 'undefined' && typeof this.context[id].valueRange !== 'undefined') {
            return this.context[id].valueRange;
        } else {
            return '[null, null]';
        }
    },

    contextHeight: function (id, def) {
        if (typeof this.context[id] !== 'undefined' && typeof this.context[id].height !== 'undefined') {
            return def * this.context[id].height;
        } else {
            return def;
        }
    },

    contextDecimalDigits: function (id, def) {
        if (typeof this.context[id] !== 'undefined' && typeof this.context[id].decimalDigits !== 'undefined') {
            return this.context[id].decimalDigits;
        } else {
            return def;
        }
    }
};

// ----------------------------------------------------------------------------

// enrich the data structure returned by netdata
// to reflect our menu system and content
// TODO: this is a shame - we should fix charts naming (issue #807)
function enrichChartData(chart) {
    var parts = chart.type.split('_');
    var tmp = parts[0];

    switch (tmp) {
        case 'ap':
        case 'net':
        case 'disk':
        case 'powersupply':
            chart.menu = tmp;
            break;

        case 'apache':
            chart.menu = chart.type;
            if (parts.length > 2 && parts[1] === 'cache') {
                chart.menu_pattern = tmp + '_' + parts[1];
            } else if (parts.length > 1) {
                chart.menu_pattern = tmp;
            }
            break;

        case 'bind':
            chart.menu = chart.type;
            if (parts.length > 2 && parts[1] === 'rndc') {
                chart.menu_pattern = tmp + '_' + parts[1];
            } else if (parts.length > 1) {
                chart.menu_pattern = tmp;
            }
            break;

        case 'cgroup':
            chart.menu = chart.type;
            if (chart.id.match(/.*[\._\/-:]qemu[\._\/-:]*/) || chart.id.match(/.*[\._\/-:]kvm[\._\/-:]*/)) {
                chart.menu_pattern = 'cgqemu';
            } else {
                chart.menu_pattern = 'cgroup';
            }
            break;

        case 'go':
            chart.menu = chart.type;
            if (parts.length > 2 && parts[1] === 'expvar') {
                chart.menu_pattern = tmp + '_' + parts[1];
            } else if (parts.length > 1) {
                chart.menu_pattern = tmp;
            }
            break;

        case 'mount':
            if (parts.length > 2) {
                chart.menu = tmp + '_' + parts[1];
            } else {
                chart.menu = tmp;
            }
            break;

        case 'isc':
            chart.menu = chart.type;
            if (parts.length > 2 && parts[1] === 'dhcpd') {
                chart.menu_pattern = tmp + '_' + parts[1];
            } else if (parts.length > 1) {
                chart.menu_pattern = tmp;
            }
            break;

        case 'ovpn':
            chart.menu = chart.type;
            if (parts.length > 3 && parts[1] === 'status' && parts[2] === 'log') {
                chart.menu_pattern = tmp + '_' + parts[1];
            } else if (parts.length > 1) {
                chart.menu_pattern = tmp;
            }
            break;

        case 'smartd':
        case 'web':
            chart.menu = chart.type;
            if (parts.length > 2 && parts[1] === 'log') {
                chart.menu_pattern = tmp + '_' + parts[1];
            } else if (parts.length > 1) {
                chart.menu_pattern = tmp;
            }
            break;

        case 'tc':
            chart.menu = tmp;

            // find a name for this device from fireqos info
            // we strip '_(in|out)' or '(in|out)_'
            if (chart.context === 'tc.qos' && (typeof options.submenu_names[chart.family] === 'undefined' || options.submenu_names[chart.family] === chart.family)) {
                var n = chart.name.split('.')[1];
                if (n.endsWith('_in')) {
                    options.submenu_names[chart.family] = n.slice(0, n.lastIndexOf('_in'));
                } else if (n.endsWith('_out')) {
                    options.submenu_names[chart.family] = n.slice(0, n.lastIndexOf('_out'));
                } else if (n.startsWith('in_')) {
                    options.submenu_names[chart.family] = n.slice(3, n.length);
                } else if (n.startsWith('out_')) {
                    options.submenu_names[chart.family] = n.slice(4, n.length);
                } else {
                    options.submenu_names[chart.family] = n;
                }
            }

            // increase the priority of IFB devices
            // to have inbound appear before outbound
            if (chart.id.match(/.*-ifb$/)) {
                chart.priority--;
            }

            break;

        default:
            chart.menu = chart.type;
            if (parts.length > 1) {
                chart.menu_pattern = tmp;
            }
            break;
    }

    chart.submenu = chart.family;
}

// ----------------------------------------------------------------------------

function headMain(os, charts, duration) {
    void (os);

    if (urlOptions.mode === 'print') {
        return '';
    }

    var head = '';

    if (typeof charts['system.swap'] !== 'undefined') {
        head += '<div class="netdata-container" style="margin-right: 10px;" data-netdata="system.swap"'
            + ' data-dimensions="used"'
            + ' data-append-options="percentage"'
            + ' data-chart-library="easypiechart"'
            + ' data-title="Used Swap"'
            + ' data-units="%"'
            + ' data-easypiechart-max-value="100"'
            + ' data-width="9%"'
            + ' data-before="0"'
            + ' data-after="-' + duration.toString() + '"'
            + ' data-points="' + duration.toString() + '"'
            + ' data-colors="#DD4400"'
            + ' role="application"></div>';
    }

    if (typeof charts['system.io'] !== 'undefined') {
        head += '<div class="netdata-container" style="margin-right: 10px;" data-netdata="system.io"'
            + ' data-dimensions="in"'
            + ' data-chart-library="easypiechart"'
            + ' data-title="Disk Read"'
            + ' data-width="11%"'
            + ' data-before="0"'
            + ' data-after="-' + duration.toString() + '"'
            + ' data-points="' + duration.toString() + '"'
            + ' data-common-units="system.io.mainhead"'
            + ' role="application"></div>';

        head += '<div class="netdata-container" style="margin-right: 10px;" data-netdata="system.io"'
            + ' data-dimensions="out"'
            + ' data-chart-library="easypiechart"'
            + ' data-title="Disk Write"'
            + ' data-width="11%"'
            + ' data-before="0"'
            + ' data-after="-' + duration.toString() + '"'
            + ' data-points="' + duration.toString() + '"'
            + ' data-common-units="system.io.mainhead"'
            + ' role="application"></div>';
    }
    else if (typeof charts['system.pgpgio'] !== 'undefined') {
        head += '<div class="netdata-container" style="margin-right: 10px;" data-netdata="system.pgpgio"'
            + ' data-dimensions="in"'
            + ' data-chart-library="easypiechart"'
            + ' data-title="Disk Read"'
            + ' data-width="11%"'
            + ' data-before="0"'
            + ' data-after="-' + duration.toString() + '"'
            + ' data-points="' + duration.toString() + '"'
            + ' data-common-units="system.pgpgio.mainhead"'
            + ' role="application"></div>';

        head += '<div class="netdata-container" style="margin-right: 10px;" data-netdata="system.pgpgio"'
            + ' data-dimensions="out"'
            + ' data-chart-library="easypiechart"'
            + ' data-title="Disk Write"'
            + ' data-width="11%"'
            + ' data-before="0"'
            + ' data-after="-' + duration.toString() + '"'
            + ' data-points="' + duration.toString() + '"'
            + ' data-common-units="system.pgpgio.mainhead"'
            + ' role="application"></div>';
    }

    if (typeof charts['system.cpu'] !== 'undefined') {
        head += '<div class="netdata-container" style="margin-right: 10px;" data-netdata="system.cpu"'
            + ' data-chart-library="gauge"'
            + ' data-title="CPU"'
            + ' data-units="%"'
            + ' data-gauge-max-value="100"'
            + ' data-width="20%"'
            + ' data-after="-' + duration.toString() + '"'
            + ' data-points="' + duration.toString() + '"'
            + ' data-colors="' + NETDATA.colors[12] + '"'
            + ' role="application"></div>';
    }

    if (typeof charts['system.net'] !== 'undefined') {
        head += '<div class="netdata-container" style="margin-right: 10px;" data-netdata="system.net"'
            + ' data-dimensions="received"'
            + ' data-chart-library="easypiechart"'
            + ' data-title="Net Inbound"'
            + ' data-width="11%"'
            + ' data-before="0"'
            + ' data-after="-' + duration.toString() + '"'
            + ' data-points="' + duration.toString() + '"'
            + ' data-common-units="system.net.mainhead"'
            + ' role="application"></div>';

        head += '<div class="netdata-container" style="margin-right: 10px;" data-netdata="system.net"'
            + ' data-dimensions="sent"'
            + ' data-chart-library="easypiechart"'
            + ' data-title="Net Outbound"'
            + ' data-width="11%"'
            + ' data-before="0"'
            + ' data-after="-' + duration.toString() + '"'
            + ' data-points="' + duration.toString() + '"'
            + ' data-common-units="system.net.mainhead"'
            + ' role="application"></div>';
    }
    else if (typeof charts['system.ip'] !== 'undefined') {
        head += '<div class="netdata-container" style="margin-right: 10px;" data-netdata="system.ip"'
            + ' data-dimensions="received"'
            + ' data-chart-library="easypiechart"'
            + ' data-title="IP Inbound"'
            + ' data-width="11%"'
            + ' data-before="0"'
            + ' data-after="-' + duration.toString() + '"'
            + ' data-points="' + duration.toString() + '"'
            + ' data-common-units="system.ip.mainhead"'
            + ' role="application"></div>';

        head += '<div class="netdata-container" style="margin-right: 10px;" data-netdata="system.ip"'
            + ' data-dimensions="sent"'
            + ' data-chart-library="easypiechart"'
            + ' data-title="IP Outbound"'
            + ' data-width="11%"'
            + ' data-before="0"'
            + ' data-after="-' + duration.toString() + '"'
            + ' data-points="' + duration.toString() + '"'
            + ' data-common-units="system.ip.mainhead"'
            + ' role="application"></div>';
    }
    else if (typeof charts['system.ipv4'] !== 'undefined') {
        head += '<div class="netdata-container" style="margin-right: 10px;" data-netdata="system.ipv4"'
            + ' data-dimensions="received"'
            + ' data-chart-library="easypiechart"'
            + ' data-title="IPv4 Inbound"'
            + ' data-width="11%"'
            + ' data-before="0"'
            + ' data-after="-' + duration.toString() + '"'
            + ' data-points="' + duration.toString() + '"'
            + ' data-common-units="system.ipv4.mainhead"'
            + ' role="application"></div>';

        head += '<div class="netdata-container" style="margin-right: 10px;" data-netdata="system.ipv4"'
            + ' data-dimensions="sent"'
            + ' data-chart-library="easypiechart"'
            + ' data-title="IPv4 Outbound"'
            + ' data-width="11%"'
            + ' data-before="0"'
            + ' data-after="-' + duration.toString() + '"'
            + ' data-points="' + duration.toString() + '"'
            + ' data-common-units="system.ipv4.mainhead"'
            + ' role="application"></div>';
    }
    else if (typeof charts['system.ipv6'] !== 'undefined') {
        head += '<div class="netdata-container" style="margin-right: 10px;" data-netdata="system.ipv6"'
            + ' data-dimensions="received"'
            + ' data-chart-library="easypiechart"'
            + ' data-title="IPv6 Inbound"'
            + ' data-units="kbps"'
            + ' data-width="11%"'
            + ' data-before="0"'
            + ' data-after="-' + duration.toString() + '"'
            + ' data-points="' + duration.toString() + '"'
            + ' data-common-units="system.ipv6.mainhead"'
            + ' role="application"></div>';

        head += '<div class="netdata-container" style="margin-right: 10px;" data-netdata="system.ipv6"'
            + ' data-dimensions="sent"'
            + ' data-chart-library="easypiechart"'
            + ' data-title="IPv6 Outbound"'
            + ' data-units="kbps"'
            + ' data-width="11%"'
            + ' data-before="0"'
            + ' data-after="-' + duration.toString() + '"'
            + ' data-points="' + duration.toString() + '"'
            + ' data-common-units="system.ipv6.mainhead"'
            + ' role="application"></div>';
    }

    if (typeof charts['system.ram'] !== 'undefined') {
        head += '<div class="netdata-container" style="margin-right: 10px;" data-netdata="system.ram"'
            + ' data-dimensions="used|buffers|active|wired"' // active and wired are FreeBSD stats
            + ' data-append-options="percentage"'
            + ' data-chart-library="easypiechart"'
            + ' data-title="Used RAM"'
            + ' data-units="%"'
            + ' data-easypiechart-max-value="100"'
            + ' data-width="9%"'
            + ' data-after="-' + duration.toString() + '"'
            + ' data-points="' + duration.toString() + '"'
            + ' data-colors="' + NETDATA.colors[7] + '"'
            + ' role="application"></div>';
    }

    return head;
}

function generateHeadCharts(type, chart, duration) {
    if (urlOptions.mode === 'print') {
        return '';
    }

    var head = '';
    var hcharts = netdataDashboard.anyAttribute(netdataDashboard.context, type, chart.context, []);
    if (hcharts.length > 0) {
        var hi = 0, hlen = hcharts.length;
        while (hi < hlen) {
            if (typeof hcharts[hi] === 'function') {
                head += hcharts[hi](netdataDashboard.os, chart.id).replace(/CHART_DURATION/g, duration.toString()).replace(/CHART_UNIQUE_ID/g, chart.id);
            } else {
                head += hcharts[hi].replace(/CHART_DURATION/g, duration.toString()).replace(/CHART_UNIQUE_ID/g, chart.id);
            }
            hi++;
        }
    }
    return head;
}

function renderPage(menus, data) {
    var div = document.getElementById('charts_div');
    var pcent_width = Math.floor(100 / chartsPerRow($(div).width()));

    // find the proper duration for per-second updates
    var duration = Math.round(($(div).width() * pcent_width / 100 * data.update_every / 3) / 60) * 60;
    options.duration = duration;
    options.update_every = data.update_every;

    var html = '';
    var sidebar = '<ul class="nav dashboard-sidenav" data-spy="affix" id="sidebar_ul">';
    var mainhead = headMain(netdataDashboard.os, data.charts, duration);

    // sort the menus
    var main = sortObjectByPriority(menus);
    var i = 0, len = main.length;
    while (i < len) {
        var menu = main[i++];

        // generate an entry at the main menu

        var menuid = NETDATA.name2id('menu_' + menu);
        sidebar += '<li class=""><a href="#' + menuid + '" onClick="return scrollToId(\'' + menuid + '\');">' + menus[menu].icon + ' ' + menus[menu].title + '</a><ul class="nav">';
        html += '<div role="section" class="dashboard-section"><div role="sectionhead"><h1 id="' + menuid + '" role="heading">' + menus[menu].icon + ' ' + menus[menu].title + '</h1></div><div role="section"  class="dashboard-subsection">';

        if (menus[menu].info !== null) {
            html += menus[menu].info;
        }

        // console.log(' >> ' + menu + ' (' + menus[menu].priority + '): ' + menus[menu].title);

        var shtml = '';
        var mhead = '<div class="netdata-chart-row">' + mainhead;
        mainhead = '';

        // sort the submenus of this menu
        var sub = sortObjectByPriority(menus[menu].submenus);
        var si = 0, slen = sub.length;
        while (si < slen) {
            var submenu = sub[si++];

            // generate an entry at the submenu
            var submenuid = NETDATA.name2id('menu_' + menu + '_submenu_' + submenu);
            sidebar += '<li class><a href="#' + submenuid + '" onClick="return scrollToId(\'' + submenuid + '\');">' + menus[menu].submenus[submenu].title + '</a></li>';
            shtml += '<div role="section" class="dashboard-section-container" id="' + submenuid + '"><h2 id="' + submenuid + '" class="netdata-chart-alignment" role="heading">' + menus[menu].submenus[submenu].title + '</h2>';

            if (menus[menu].submenus[submenu].info !== null) {
                shtml += '<div class="dashboard-submenu-info netdata-chart-alignment" role="document">' + menus[menu].submenus[submenu].info + '</div>';
            }

            var head = '<div class="netdata-chart-row">';
            var chtml = '';

            // console.log('    \------- ' + submenu + ' (' + menus[menu].submenus[submenu].priority + '): ' + menus[menu].submenus[submenu].title);

            // sort the charts in this submenu of this menu
            menus[menu].submenus[submenu].charts.sort(prioritySort);
            var ci = 0, clen = menus[menu].submenus[submenu].charts.length;
            while (ci < clen) {
                var chart = menus[menu].submenus[submenu].charts[ci++];

                // generate the submenu heading charts
                mhead += generateHeadCharts('mainheads', chart, duration);
                head += generateHeadCharts('heads', chart, duration);

                function chartCommonMin(family, context, units) {
                    var x = netdataDashboard.anyAttribute(netdataDashboard.context, 'commonMin', context, undefined);
                    if (typeof x !== 'undefined') {
                        return ' data-common-min="' + family + '/' + context + '/' + units + '"';
                    } else {
                        return '';
                    }
                }

                function chartCommonMax(family, context, units) {
                    var x = netdataDashboard.anyAttribute(netdataDashboard.context, 'commonMax', context, undefined);
                    if (typeof x !== 'undefined') {
                        return ' data-common-max="' + family + '/' + context + '/' + units + '"';
                    } else {
                        return '';
                    }
                }

                // generate the chart
                if (urlOptions.mode === 'print') {
                    chtml += '<div role="row" class="dashboard-print-row">';
                }

                chtml += '<div class="netdata-chartblock-container" style="width: ' + pcent_width.toString() + '%;">' + netdataDashboard.contextInfo(chart.context) + '<div class="netdata-container" id="chart_' + NETDATA.name2id(chart.id) + '" data-netdata="' + chart.id + '"'
                    + ' data-width="100%"'
                    + ' data-height="' + netdataDashboard.contextHeight(chart.context, options.chartsHeight).toString() + 'px"'
                    + ' data-dygraph-valuerange="' + netdataDashboard.contextValueRange(chart.context) + '"'
                    + ' data-before="0"'
                    + ' data-after="-' + duration.toString() + '"'
                    + ' data-id="' + NETDATA.name2id(options.hostname + '/' + chart.id) + '"'
                    + ' data-colors="' + netdataDashboard.anyAttribute(netdataDashboard.context, 'colors', chart.context, '') + '"'
                    + ' data-decimal-digits="' + netdataDashboard.contextDecimalDigits(chart.context, -1) + '"'
                    + chartCommonMin(chart.family, chart.context, chart.units)
                    + chartCommonMax(chart.family, chart.context, chart.units)
                    + ' role="application"></div></div>';

                if (urlOptions.mode === 'print') {
                    chtml += '</div>';
                }
            }

            head += '</div>';
            shtml += head + chtml + '</div>';
        }

        mhead += '</div>';
        sidebar += '</ul></li>';
        html += mhead + shtml + '</div></div><hr role="separator"/>';
    }

    const isMemoryModeDbEngine = data.memory_mode === "dbengine";

    sidebar += '<li class="" style="padding-top:15px;"><a href="https://learn.netdata.cloud/docs/agent/collectors/quickstart/" target="_blank"><i class="fas fa-plus"></i> Add more charts</a></li>';
    sidebar += '<li class=""><a href="https://learn.netdata.cloud/docs/agent/health/quickstart/" target="_blank"><i class="fas fa-plus"></i> Add more alarms</a></li>';
    sidebar += '<li class="" style="margin:20px;color:#666;"><small>Every ' +
      ((data.update_every === 1) ? 'second' : data.update_every.toString() + ' seconds') + ', ' +
      'Netdata collects <strong>' + data.dimensions_count.toLocaleString() + '</strong> metrics on ' +
      data.hostname.toString() + ', presents them in <strong>' +
      data.charts_count.toLocaleString() + '</strong> charts' +
      (isMemoryModeDbEngine ? '' : ',') + // oxford comma
      ' and monitors them with <strong>' +
      data.alarms_count.toLocaleString() + '</strong> alarms.';

    if (!isMemoryModeDbEngine) {
        sidebar += '<br />&nbsp;<br />Get more history by ' +
          '<a href="https://learn.netdata.cloud/guides/longer-metrics-storage#using-the-round-robin-database" target=_blank>configuring Netdata\'s <strong>history</strong></a> or using the <a href="https://learn.netdata.cloud/docs/agent/database/engine/" target=_blank>DB engine.</a>';
    }

    sidebar += '<br/>&nbsp;<br/><strong>netdata</strong><br/>' + data.version.toString() + '</small></li>';

    sidebar += '</ul>';
    div.innerHTML = html;
    document.getElementById('sidebar').innerHTML = sidebar;

    if (urlOptions.highlight === true) {
        NETDATA.globalChartUnderlay.init(null
            , urlOptions.highlight_after
            , urlOptions.highlight_before
            , (urlOptions.after > 0) ? urlOptions.after : null
            , (urlOptions.before > 0) ? urlOptions.before : null
        );
    } else {
        NETDATA.globalChartUnderlay.clear();
    }

    if (urlOptions.mode === 'print') {
        printPage();
    } else {
        finalizePage();
    }
}

function renderChartsAndMenu(data) {
    options.menus = {};
    options.submenu_names = {};

    var menus = options.menus;
    var charts = data.charts;
    var m, menu_key;

    for (var c in charts) {
        if (!charts.hasOwnProperty(c)) {
            continue;
        }

        var chart = charts[c];
        enrichChartData(chart);
        m = chart.menu;

        // create the menu
        if (typeof menus[m] === 'undefined') {
            menus[m] = {
                menu_pattern: chart.menu_pattern,
                priority: chart.priority,
                submenus: {},
                title: netdataDashboard.menuTitle(chart),
                icon: netdataDashboard.menuIcon(chart),
                info: netdataDashboard.menuInfo(chart),
                height: netdataDashboard.menuHeight(chart) * options.chartsHeight
            };
        } else {
            if (typeof (menus[m].menu_pattern) === 'undefined') {
                menus[m].menu_pattern = chart.menu_pattern;
            }

            if (chart.priority < menus[m].priority) {
                menus[m].priority = chart.priority;
            }
        }

        menu_key = (typeof (menus[m].menu_pattern) !== 'undefined') ? menus[m].menu_pattern : m;

        // create the submenu
        if (typeof menus[m].submenus[chart.submenu] === 'undefined') {
            menus[m].submenus[chart.submenu] = {
                priority: chart.priority,
                charts: [],
                title: null,
                info: netdataDashboard.submenuInfo(menu_key, chart.submenu),
                height: netdataDashboard.submenuHeight(menu_key, chart.submenu, menus[m].height)
            };
        } else {
            if (chart.priority < menus[m].submenus[chart.submenu].priority) {
                menus[m].submenus[chart.submenu].priority = chart.priority;
            }
        }

        // index the chart in the menu/submenu
        menus[m].submenus[chart.submenu].charts.push(chart);
    }

    // propagate the descriptive subname given to QoS
    // to all the other submenus with the same name
    for (var m in menus) {
        if (!menus.hasOwnProperty(m)) {
            continue;
        }

        for (var s in menus[m].submenus) {
            if (!menus[m].submenus.hasOwnProperty(s)) {
                continue;
            }

            // set the family using a name
            if (typeof options.submenu_names[s] !== 'undefined') {
                menus[m].submenus[s].title = s + ' (' + options.submenu_names[s] + ')';
            } else {
                menu_key = (typeof (menus[m].menu_pattern) !== 'undefined') ? menus[m].menu_pattern : m;
                menus[m].submenus[s].title = netdataDashboard.submenuTitle(menu_key, s);
            }
        }
    }

    renderPage(menus, data);
}

// ----------------------------------------------------------------------------

function loadJs(url, callback) {
    $.ajax({
        url: url.startsWith("http") ? url : transformWithOldSuffix(url),
        cache: true,
        dataType: "script",
        xhrFields: { withCredentials: true } // required for the cookie
    })
        .fail(function () {
            alert('Cannot load required JS library: ' + url);
        })
        .always(function () {
            if (typeof callback === 'function') {
                callback();
            }
        })
}

var clipboardLoaded = false;

function loadClipboard(callback) {
    if (clipboardLoaded === false) {
        clipboardLoaded = true;
        loadJs('lib/clipboard-polyfill-be05dad.js', callback);
    } else {
        callback();
    }
}

var bootstrapTableLoaded = false;

function loadBootstrapTable(callback) {
    if (bootstrapTableLoaded === false) {
        bootstrapTableLoaded = true;
        loadJs('lib/bootstrap-table-1.11.0.min.js', function () {
            loadJs('lib/bootstrap-table-export-1.11.0.min.js', function () {
                loadJs('lib/tableExport-1.6.0.min.js', callback);
            })
        });
    } else {
        callback();
    }
}

var bootstrapSliderLoaded = false;

function loadBootstrapSlider(callback) {
    if (bootstrapSliderLoaded === false) {
        bootstrapSliderLoaded = true;
        loadJs('lib/bootstrap-slider-10.0.0.min.js', function () {
            NETDATA._loadCSS(transformWithOldSuffix("css/bootstrap-slider-10.0.0.min.css"));
            callback();
        });
    } else {
        callback();
    }
}

var lzStringLoaded = false;

function loadLzString(callback) {
    if (lzStringLoaded === false) {
        lzStringLoaded = true;
        loadJs('lib/lz-string-1.4.4.min.js', callback);
    } else {
        callback();
    }
}

var pakoLoaded = false;

function loadPako(callback) {
    if (pakoLoaded === false) {
        pakoLoaded = true;
        loadJs('lib/pako-1.0.6.min.js', callback);
    } else {
        callback();
    }
}

// ----------------------------------------------------------------------------

function clipboardCopy(text) {
    clipboard.writeText(text);
}

function clipboardCopyBadgeEmbed(url) {
    clipboard.writeText('<embed src="' + url + '" type="image/svg+xml" height="20"/>');
}

// ----------------------------------------------------------------------------

function alarmsUpdateModal() {
    var active = '<h3>Raised Alarms</h3><table class="table">';
    var all = '<h3>All Running Alarms</h3><div class="panel-group" id="alarms_all_accordion" role="tablist" aria-multiselectable="true">';
    var footer = '<hr/><a href="https://github.com/netdata/netdata/tree/master/src/web/api/badges#netdata-badges" target="_blank">netdata badges</a> refresh automatically. Their color indicates the state of the alarm: <span style="color: #e05d44"><b>&nbsp;red&nbsp;</b></span> is critical, <span style="color:#fe7d37"><b>&nbsp;orange&nbsp;</b></span> is warning, <span style="color: #4c1"><b>&nbsp;bright green&nbsp;</b></span> is ok, <span style="color: #9f9f9f"><b>&nbsp;light grey&nbsp;</b></span> is undefined (i.e. no data or no status), <span style="color: #000"><b>&nbsp;black&nbsp;</b></span> is not initialized. You can copy and paste their URLs to embed them in any web page.<br/>netdata can send notifications for these alarms. Check <a href="https://github.com/netdata/netdata/blob/master/src/health/notifications/health_alarm_notify.conf" target="_blank">this configuration file</a> for more information.';

    loadClipboard(function () {
    });

    NETDATA.alarms.get('all', function (data) {
        options.alarm_families = [];

        alarmsCallback(data);

        if (data === null) {
            document.getElementById('alarms_active').innerHTML =
                document.getElementById('alarms_all').innerHTML =
                document.getElementById('alarms_log').innerHTML =
                'failed to load alarm data!';
            return;
        }

        function alarmid4human(id) {
            if (id === 0) {
                return '-';
            }

            return id.toString();
        }

        function timestamp4human(timestamp, space) {
            if (timestamp === 0) {
                return '-';
            }

            if (typeof space === 'undefined') {
                space = '&nbsp;';
            }

            var t = new Date(timestamp * 1000);
            var now = new Date();

            if (t.toDateString() === now.toDateString()) {
                return t.toLocaleTimeString();
            }

            return t.toLocaleDateString() + space + t.toLocaleTimeString();
        }

        function alarm_lookup_explain(alarm, chart) {
            var dimensions = ' of all values ';

            if (chart.dimensions.length > 1) {
                dimensions = ' of the sum of all dimensions ';
            }

            if (typeof alarm.lookup_dimensions !== 'undefined') {
                var d = alarm.lookup_dimensions.replace(/|/g, ',');
                var x = d.split(',');
                if (x.length > 1) {
                    dimensions = 'of the sum of dimensions <code>' + alarm.lookup_dimensions + '</code> ';
                } else {
                    dimensions = 'of all values of dimension <code>' + alarm.lookup_dimensions + '</code> ';
                }
            }

            return '<code>' + alarm.lookup_method + '</code> '
                + dimensions + ', of chart <code>' + alarm.chart + '</code>'
                + ', starting <code>' + NETDATA.seconds4human(alarm.lookup_after + alarm.lookup_before, { space: '&nbsp;' }) + '</code> and up to <code>' + NETDATA.seconds4human(alarm.lookup_before, { space: '&nbsp;' }) + '</code>'
                + ((alarm.lookup_options) ? (', with options <code>' + alarm.lookup_options.replace(/ /g, ',&nbsp;') + '</code>') : '')
                + '.';
        }

        function alarm_to_html(alarm, full) {
            var chart = options.data.charts[alarm.chart];
            if (typeof (chart) === 'undefined') {
                chart = options.data.charts_by_name[alarm.chart];
                if (typeof (chart) === 'undefined') {
                    // this means the charts loaded are incomplete
                    // probably netdata was restarted and more alarms
                    // are now available.
                    console.log('Cannot find chart ' + alarm.chart + ', you probably need to refresh the page.');
                    return '';
                }
            }

            var has_alarm = (typeof alarm.warn !== 'undefined' || typeof alarm.crit !== 'undefined');
            var badge_url = NETDATA.alarms.server + '/api/v1/badge.svg?chart=' + alarm.chart + '&alarm=' + alarm.name + '&refresh=auto';

            var action_buttons = '<br/>&nbsp;<br/>role: <b>' + alarm.recipient + '</b><br/>&nbsp;<br/>'
                + '<div class="action-button ripple" title="click to scroll the dashboard to the chart of this alarm" data-toggle="tooltip" data-placement="bottom" onClick="scrollToChartAfterHidingModal(\'' + alarm.chart + '\', ' + alarm.last_status_change * 1000 + ', \'' + alarm.status + '\'); $(\'#alarmsModal\').modal(\'hide\'); return false;"><i class="fab fa-periscope"></i></div>'
                + '<div class="action-button ripple" title="click to copy to the clipboard the URL of this badge" data-toggle="tooltip" data-placement="bottom" onClick="clipboardCopy(\'' + badge_url + '\'); return false;"><i class="far fa-copy"></i></div>'
                + '<div class="action-button ripple" title="click to copy to the clipboard an auto-refreshing <code>embed</code> html element for this badge" data-toggle="tooltip" data-placement="bottom" onClick="clipboardCopyBadgeEmbed(\'' + badge_url + '\'); return false;"><i class="fas fa-copy"></i></div>';

            var html = '<tr><td class="text-center" style="vertical-align:middle" width="40%"><b>' + alarm.chart + '</b><br/>&nbsp;<br/><embed src="' + badge_url + '" type="image/svg+xml" height="20"/><br/>&nbsp;<br/><span style="font-size: 18px">' + alarm.info + '</span>' + action_buttons + '</td>'
                + '<td><table class="table">'
                + ((typeof alarm.warn !== 'undefined') ? ('<tr><td width="10%" style="text-align:right">warning&nbsp;when</td><td><span style="font-family: monospace; color:#fe7d37; font-weight: bold;">' + alarm.warn + '</span></td></tr>') : '')
                + ((typeof alarm.crit !== 'undefined') ? ('<tr><td width="10%" style="text-align:right">critical&nbsp;when</td><td><span style="font-family: monospace; color: #e05d44; font-weight: bold;">' + alarm.crit + '</span></td></tr>') : '');

            if (full === true) {
                var units = chart.units;
                if (units === '%') {
                    units = '&#37;';
                }

                html += ((typeof alarm.lookup_after !== 'undefined') ? ('<tr><td width="10%" style="text-align:right">db&nbsp;lookup</td><td>' + alarm_lookup_explain(alarm, chart) + '</td></tr>') : '')
                    + ((typeof alarm.calc !== 'undefined') ? ('<tr><td width="10%" style="text-align:right">calculation</td><td><span style="font-family: monospace;">' + alarm.calc + '</span></td></tr>') : '')
                    + ((chart.green !== null) ? ('<tr><td width="10%" style="text-align:right">green&nbsp;threshold</td><td><code>' + chart.green + ' ' + units + '</code></td></tr>') : '')
                    + ((chart.red !== null) ? ('<tr><td width="10%" style="text-align:right">red&nbsp;threshold</td><td><code>' + chart.red + ' ' + units + '</code></td></tr>') : '');
            }

            if (alarm.warn_repeat_every > 0) {
                html += '<tr><td width="10%" style="text-align:right">repeat&nbsp;warning</td><td>' + NETDATA.seconds4human(alarm.warn_repeat_every) + '</td></tr>';
            }

            if (alarm.crit_repeat_every > 0) {
                html += '<tr><td width="10%" style="text-align:right">repeat&nbsp;critical</td><td>' + NETDATA.seconds4human(alarm.crit_repeat_every) + '</td></tr>';
            }

            var delay = '';
            if ((alarm.delay_up_duration > 0 || alarm.delay_down_duration > 0) && alarm.delay_multiplier !== 0 && alarm.delay_max_duration > 0) {
                if (alarm.delay_up_duration === alarm.delay_down_duration) {
                    delay += '<small><br/>hysteresis ' + NETDATA.seconds4human(alarm.delay_up_duration, {
                        space: '&nbsp;',
                        negative_suffix: ''
                    });
                } else {
                    delay = '<small><br/>hysteresis ';
                    if (alarm.delay_up_duration > 0) {
                        delay += 'on&nbsp;escalation&nbsp;<code>' + NETDATA.seconds4human(alarm.delay_up_duration, {
                            space: '&nbsp;',
                            negative_suffix: ''
                        }) + '</code>, ';
                    }
                    if (alarm.delay_down_duration > 0) {
                        delay += 'on&nbsp;recovery&nbsp;<code>' + NETDATA.seconds4human(alarm.delay_down_duration, {
                            space: '&nbsp;',
                            negative_suffix: ''
                        }) + '</code>, ';
                    }
                }
                if (alarm.delay_multiplier !== 1.0) {
                    delay += 'multiplied&nbsp;by&nbsp;<code>' + alarm.delay_multiplier.toString() + '</code>';
                    delay += ',&nbsp;up&nbsp;to&nbsp;<code>' + NETDATA.seconds4human(alarm.delay_max_duration, {
                        space: '&nbsp;',
                        negative_suffix: ''
                    }) + '</code>';
                }
                delay += '</small>';
            }

            html += '<tr><td width="10%" style="text-align:right">check&nbsp;every</td><td>' + NETDATA.seconds4human(alarm.update_every, {
                space: '&nbsp;',
                negative_suffix: ''
            }) + '</td></tr>'
                + ((has_alarm === true) ? ('<tr><td width="10%" style="text-align:right">execute</td><td><span style="font-family: monospace;">' + alarm.exec + '</span>' + delay + '</td></tr>') : '')
                + '<tr><td width="10%" style="text-align:right">source</td><td><span style="font-family: monospace;">' + alarm.source + '</span></td></tr>'
                + '</table></td></tr>';

            return html;
        }

        function alarm_family_show(id) {
            var html = '<table class="table">';
            var family = options.alarm_families[id];
            var len = family.arr.length;
            while (len--) {
                var alarm = family.arr[len];
                html += alarm_to_html(alarm, true);
            }
            html += '</table>';

            $('#alarm_all_' + id.toString()).html(html);
            enableTooltipsAndPopovers();
        }

        // find the proper family of each alarm
        var x, family, alarm;
        var count_active = 0;
        var count_all = 0;
        var families = {};
        var families_sort = [];
        for (x in data.alarms) {
            if (!data.alarms.hasOwnProperty(x)) {
                continue;
            }

            alarm = data.alarms[x];
            family = alarm.family;

            // find the chart
            var chart = options.data.charts[alarm.chart];
            if (typeof chart === 'undefined') {
                chart = options.data.charts_by_name[alarm.chart];
            }

            // not found - this should never happen!
            if (typeof chart === 'undefined') {
                console.log('WARNING: alarm ' + x + ' is linked to chart ' + alarm.chart + ', which is not found in the list of chart got from the server.');
                chart = { priority: 9999999 };
            }
            else if (typeof chart.menu !== 'undefined' && typeof chart.submenu !== 'undefined')
            // the family based on the chart
            {
                family = chart.menu + ' - ' + chart.submenu;
            }

            if (typeof families[family] === 'undefined') {
                families[family] = {
                    name: family,
                    arr: [],
                    priority: chart.priority
                };

                families_sort.push(families[family]);
            }

            if (chart.priority < families[family].priority) {
                families[family].priority = chart.priority;
            }

            families[family].arr.unshift(alarm);
        }

        // sort the families, like the dashboard menu does
        var families_sorted = families_sort.sort(function (a, b) {
            if (a.priority < b.priority) {
                return -1;
            }
            if (a.priority > b.priority) {
                return 1;
            }
            return naturalSortCompare(a.name, b.name);
        });

        var i = 0;
        var fc = 0;
        var len = families_sorted.length;
        while (len--) {
            family = families_sorted[i++].name;
            var active_family_added = false;
            var expanded = 'true';
            var collapsed = '';
            var cin = 'in';

            if (fc !== 0) {
                all += "</table></div></div></div>";
                expanded = 'false';
                collapsed = 'class="collapsed"';
                cin = '';
            }

            all += '<div class="panel panel-default"><div class="panel-heading" role="tab" id="alarm_all_heading_' + fc.toString() + '"><h4 class="panel-title"><a ' + collapsed + ' role="button" data-toggle="collapse" data-parent="#alarms_all_accordion" href="#alarm_all_' + fc.toString() + '" aria-expanded="' + expanded + '" aria-controls="alarm_all_' + fc.toString() + '">' + family.toString() + '</a></h4></div><div id="alarm_all_' + fc.toString() + '" class="panel-collapse collapse ' + cin + '" role="tabpanel" aria-labelledby="alarm_all_heading_' + fc.toString() + '" data-alarm-id="' + fc.toString() + '"><div class="panel-body" id="alarm_all_body_' + fc.toString() + '">';

            options.alarm_families[fc] = families[family];

            fc++;

            var arr = families[family].arr;
            var c = arr.length;
            while (c--) {
                alarm = arr[c];
                if (alarm.status === 'WARNING' || alarm.status === 'CRITICAL') {
                    if (!active_family_added) {
                        active_family_added = true;
                        active += '<tr><th class="text-center" colspan="2"><h4>' + family + '</h4></th></tr>';
                    }
                    count_active++;
                    active += alarm_to_html(alarm, true);
                }

                count_all++;
            }
        }
        active += "</table>";
        if (families_sorted.length > 0) {
            all += "</div></div></div>";
        }
        all += "</div>";

        if (!count_active) {
            active += '<div style="width:100%; height: 100px; text-align: center;"><span style="font-size: 50px;"><i class="fas fa-thumbs-up"></i></span><br/>Everything is normal. No raised alarms.</div>';
        } else {
            active += footer;
        }

        if (!count_all) {
            all += "<h4>No alarms are running in this system.</h4>";
        } else {
            all += footer;
        }

        document.getElementById('alarms_active').innerHTML = active;
        document.getElementById('alarms_all').innerHTML = all;
        enableTooltipsAndPopovers();

        if (families_sorted.length > 0) {
            alarm_family_show(0);
        }

        // register bootstrap events
        var $accordion = $('#alarms_all_accordion');
        $accordion.on('show.bs.collapse', function (d) {
            var target = $(d.target);
            var id = $(target).data('alarm-id');
            alarm_family_show(id);
        });
        $accordion.on('hidden.bs.collapse', function (d) {
            var target = $(d.target);
            var id = $(target).data('alarm-id');
            $('#alarm_all_' + id.toString()).html('');
        });

        document.getElementById('alarms_log').innerHTML = '<h3>Alarm Log</h3><table id="alarms_log_table"></table>';

        loadBootstrapTable(function () {
            $('#alarms_log_table').bootstrapTable({
                url: NETDATA.alarms.server + '/api/v1/alarm_log?all',
                cache: false,
                pagination: true,
                pageSize: 10,
                showPaginationSwitch: false,
                search: true,
                searchTimeOut: 300,
                searchAlign: 'left',
                showColumns: true,
                showExport: true,
                exportDataType: 'basic',
                exportOptions: {
                    fileName: 'netdata_alarm_log'
                },
                onClickRow: function (row, $element,field) {
                    void (field);
                    void ($element);
                    let main_url;
                    let common_url = "&host=" + encodeURIComponent(row['hostname']) + "&chart=" + encodeURIComponent(row['chart']) + "&family=" + encodeURIComponent(row['family']) + "&alarm=" + encodeURIComponent(row['name']) + "&alarm_unique_id=" + row['unique_id'] + "&alarm_id=" + row['alarm_id'] + "&alarm_event_id=" +  row['alarm_event_id'] + "&alarm_when=" + row['when'];
                    if (NETDATA.registry.isUsingGlobalRegistry() && NETDATA.registry.machine_guid != null) {
                        main_url = "https://netdata.cloud/alarms/redirect?agentID=" + NETDATA.registry.machine_guid + common_url;
                    } else {
                        main_url = NETDATA.registry.server + "/goto-host-from-alarm.html?" + common_url ;
                    }
                    window.open(main_url,"_blank");
                },
                rowStyle: function (row, index) {
                    void (index);

                    switch (row.status) {
                        case 'CRITICAL':
                            return { classes: 'danger' };
                            break;
                        case 'WARNING':
                            return { classes: 'warning' };
                            break;
                        case 'UNDEFINED':
                            return { classes: 'info' };
                            break;
                        case 'CLEAR':
                            return { classes: 'success' };
                            break;
                    }
                    return {};
                },
                showFooter: false,
                showHeader: true,
                showRefresh: true,
                showToggle: false,
                sortable: true,
                silentSort: false,
                columns: [
                    {
                        field: 'when',
                        title: 'Event Date',
                        valign: 'middle',
                        titleTooltip: 'The date and time the even took place',
                        formatter: function (value, row, index) {
                            void (row);
                            void (index);
                            return timestamp4human(value, ' ');
                        },
                        align: 'center',
                        switchable: false,
                        sortable: true
                    },
                    {
                        field: 'hostname',
                        title: 'Host',
                        valign: 'middle',
                        titleTooltip: 'The host that generated this event',
                        align: 'center',
                        visible: false,
                        sortable: true
                    },
                    {
                        field: 'unique_id',
                        title: 'Unique ID',
                        titleTooltip: 'The host unique ID for this event',
                        formatter: function (value, row, index) {
                            void (row);
                            void (index);
                            return alarmid4human(value);
                        },
                        align: 'center',
                        valign: 'middle',
                        visible: false,
                        sortable: true
                    },
                    {
                        field: 'alarm_id',
                        title: 'Alarm ID',
                        titleTooltip: 'The ID of the alarm that generated this event',
                        formatter: function (value, row, index) {
                            void (row);
                            void (index);
                            return alarmid4human(value);
                        },
                        align: 'center',
                        valign: 'middle',
                        visible: false,
                        sortable: true
                    },
                    {
                        field: 'alarm_event_id',
                        title: 'Alarm Event ID',
                        titleTooltip: 'The incremental ID of this event for the given alarm',
                        formatter: function (value, row, index) {
                            void (row);
                            void (index);
                            return alarmid4human(value);
                        },
                        align: 'center',
                        valign: 'middle',
                        visible: false,
                        sortable: true
                    },
                    {
                        field: 'chart',
                        title: 'Chart',
                        titleTooltip: 'The chart the alarm is attached to',
                        align: 'center',
                        valign: 'middle',
                        switchable: false,
                        sortable: true
                    },
                    {
                        field: 'family',
                        title: 'Family',
                        titleTooltip: 'The family of the chart the alarm is attached to',
                        align: 'center',
                        valign: 'middle',
                        visible: false,
                        sortable: true
                    },
                    {
                        field: 'name',
                        title: 'Alarm',
                        titleTooltip: 'The alarm name that generated this event',
                        formatter: function (value, row, index) {
                            void (row);
                            void (index);
                            return value.toString().replace(/_/g, ' ');
                        },
                        align: 'center',
                        valign: 'middle',
                        switchable: false,
                        sortable: true
                    },
                    {
                        field: 'value_string',
                        title: 'Friendly Value',
                        titleTooltip: 'The value of the alarm, that triggered this event',
                        align: 'right',
                        valign: 'middle',
                        sortable: true
                    },
                    {
                        field: 'old_value_string',
                        title: 'Friendly Old Value',
                        titleTooltip: 'The value of the alarm, just before this event',
                        align: 'right',
                        valign: 'middle',
                        visible: false,
                        sortable: true
                    },
                    {
                        field: 'old_value',
                        title: 'Old Value',
                        titleTooltip: 'The value of the alarm, just before this event',
                        formatter: function (value, row, index) {
                            void (row);
                            void (index);
                            return ((value !== null) ? Math.round(value * 100) / 100 : 'NaN').toString();
                        },
                        align: 'center',
                        valign: 'middle',
                        visible: false,
                        sortable: true
                    },
                    {
                        field: 'value',
                        title: 'Value',
                        titleTooltip: 'The value of the alarm, that triggered this event',
                        formatter: function (value, row, index) {
                            void (row);
                            void (index);
                            return ((value !== null) ? Math.round(value * 100) / 100 : 'NaN').toString();
                        },
                        align: 'right',
                        valign: 'middle',
                        visible: false,
                        sortable: true
                    },
                    {
                        field: 'units',
                        title: 'Units',
                        titleTooltip: 'The units of the value of the alarm',
                        align: 'left',
                        valign: 'middle',
                        visible: false,
                        sortable: true
                    },
                    {
                        field: 'old_status',
                        title: 'Old Status',
                        titleTooltip: 'The status of the alarm, just before this event',
                        align: 'center',
                        valign: 'middle',
                        visible: false,
                        sortable: true
                    },
                    {
                        field: 'status',
                        title: 'Status',
                        titleTooltip: 'The status of the alarm, that was set due to this event',
                        align: 'center',
                        valign: 'middle',
                        switchable: false,
                        sortable: true
                    },
                    {
                        field: 'duration',
                        title: 'Last Duration',
                        titleTooltip: 'The duration the alarm was at its previous state, just before this event',
                        formatter: function (value, row, index) {
                            void (row);
                            void (index);
                            return NETDATA.seconds4human(value, { negative_suffix: '', space: ' ', now: 'no time' });
                        },
                        align: 'center',
                        valign: 'middle',
                        visible: false,
                        sortable: true
                    },
                    {
                        field: 'non_clear_duration',
                        title: 'Raised Duration',
                        titleTooltip: 'The duration the alarm was raised, just before this event',
                        formatter: function (value, row, index) {
                            void (row);
                            void (index);
                            return NETDATA.seconds4human(value, { negative_suffix: '', space: ' ', now: 'no time' });
                        },
                        align: 'center',
                        valign: 'middle',
                        visible: false,
                        sortable: true
                    },
                    {
                        field: 'recipient',
                        title: 'Recipient',
                        titleTooltip: 'The recipient of this event',
                        align: 'center',
                        valign: 'middle',
                        visible: false,
                        sortable: true
                    },
                    {
                        field: 'processed',
                        title: 'Processed Status',
                        titleTooltip: 'True when this event is processed',
                        formatter: function (value, row, index) {
                            void (row);
                            void (index);

                            if (value === true) {
                                return 'DONE';
                            } else {
                                return 'PENDING';
                            }
                        },
                        align: 'center',
                        valign: 'middle',
                        visible: false,
                        sortable: true
                    },
                    {
                        field: 'updated',
                        title: 'Updated Status',
                        titleTooltip: 'True when this event has been updated by another event',
                        formatter: function (value, row, index) {
                            void (row);
                            void (index);

                            if (value === true) {
                                return 'UPDATED';
                            } else {
                                return 'CURRENT';
                            }
                        },
                        align: 'center',
                        valign: 'middle',
                        visible: false,
                        sortable: true
                    },
                    {
                        field: 'updated_by_id',
                        title: 'Updated By ID',
                        titleTooltip: 'The unique ID of the event that obsoleted this one',
                        formatter: function (value, row, index) {
                            void (row);
                            void (index);
                            return alarmid4human(value);
                        },
                        align: 'center',
                        valign: 'middle',
                        visible: false,
                        sortable: true
                    },
                    {
                        field: 'updates_id',
                        title: 'Updates ID',
                        titleTooltip: 'The unique ID of the event obsoleted because of this event',
                        formatter: function (value, row, index) {
                            void (row);
                            void (index);
                            return alarmid4human(value);
                        },
                        align: 'center',
                        valign: 'middle',
                        visible: false,
                        sortable: true
                    },
                    {
                        field: 'exec',
                        title: 'Script',
                        titleTooltip: 'The script to handle the event notification',
                        align: 'center',
                        valign: 'middle',
                        visible: false,
                        sortable: true
                    },
                    {
                        field: 'exec_run',
                        title: 'Script Run At',
                        titleTooltip: 'The date and time the script has been ran',
                        formatter: function (value, row, index) {
                            void (row);
                            void (index);
                            return timestamp4human(value, ' ');
                        },
                        align: 'center',
                        valign: 'middle',
                        visible: false,
                        sortable: true
                    },
                    {
                        field: 'exec_code',
                        title: 'Script Return Value',
                        titleTooltip: 'The return code of the script',
                        formatter: function (value, row, index) {
                            void (row);
                            void (index);

                            if (value === 0) {
                                return 'OK (returned 0)';
                            } else {
                                return 'FAILED (with code ' + value.toString() + ')';
                            }
                        },
                        align: 'center',
                        valign: 'middle',
                        visible: false,
                        sortable: true
                    },
                    {
                        field: 'delay',
                        title: 'Script Delay',
                        titleTooltip: 'The hysteresis of the notification',
                        formatter: function (value, row, index) {
                            void (row);
                            void (index);

                            return NETDATA.seconds4human(value, { negative_suffix: '', space: ' ', now: 'no time' });
                        },
                        align: 'center',
                        valign: 'middle',
                        visible: false,
                        sortable: true
                    },
                    {
                        field: 'delay_up_to_timestamp',
                        title: 'Script Delay Run At',
                        titleTooltip: 'The date and time the script should be run, after hysteresis',
                        formatter: function (value, row, index) {
                            void (row);
                            void (index);
                            return timestamp4human(value, ' ');
                        },
                        align: 'center',
                        valign: 'middle',
                        visible: false,
                        sortable: true
                    },
                    {
                        field: 'info',
                        title: 'Description',
                        titleTooltip: 'A short description of the alarm',
                        align: 'center',
                        valign: 'middle',
                        visible: false,
                        sortable: true
                    },
                    {
                        field: 'source',
                        title: 'Alarm Source',
                        titleTooltip: 'The source of configuration of the alarm',
                        align: 'center',
                        valign: 'middle',
                        visible: false,
                        sortable: true
                    }
                ]
            });
            // console.log($('#alarms_log_table').bootstrapTable('getOptions'));
        });
    });
}

function alarmsCallback(data) {
    var count = 0, x;
    for (x in data.alarms) {
        if (!data.alarms.hasOwnProperty(x)) {
            continue;
        }

        var alarm = data.alarms[x];
        if (alarm.status === 'WARNING' || alarm.status === 'CRITICAL') {
            count++;
        }
    }

    if (count > 0) {
        document.getElementById('alarms_count_badge').innerHTML = count.toString();
    } else {
        document.getElementById('alarms_count_badge').innerHTML = '';
    }
}

function initializeDynamicDashboardWithData(data) {
    if (data !== null) {
        options.hostname = data.hostname;
        options.data = data;
        options.version = data.version;
        options.release_channel = data.release_channel;
        netdataDashboard.os = data.os;

        if (typeof data.hosts !== 'undefined') {
            options.hosts = data.hosts;
        }

        // update the dashboard hostname
        document.getElementById('hostname').innerHTML = '<span id="hostnametext">' + options.hostname + ((netdataSnapshotData !== null) ? ' (snap)' : '').toString() + '</span>&nbsp;&nbsp;<strong class="caret">';
        document.getElementById('hostname').href = NETDATA.serverDefault;
        document.getElementById('netdataVersion').innerHTML = options.version;

        if (netdataSnapshotData !== null) {
            $('#alarmsButton').hide();
            $('#updateButton').hide();
            // $('#loadButton').hide();
            $('#saveButton').hide();
            $('#printButton').hide();
        }

        // update the dashboard title
        document.title = options.hostname + ' netdata dashboard';

        // close the splash screen
        $("#loadOverlay").css("display", "none");

        // create a chart_by_name index
        data.charts_by_name = {};
        var charts = data.charts;
        var x;
        for (x in charts) {
            if (!charts.hasOwnProperty(x)) {
                continue;
            }

            var chart = charts[x];
            data.charts_by_name[chart.name] = chart;
        }

        // render all charts
        renderChartsAndMenu(data);

        // Ensure MyNetdata menu is rendered with latest host info #5370
        renderMyNetdataMenu(isSignedIn() ? cloudAgents : registryAgents);
    }
}

// an object to keep initialization configuration
// needed due to the async nature of the XSS modal
var initializeConfig = {
    url: null,
    custom_info: true,
};

function loadCustomDashboardInfo(url, callback) {
    loadJs(url, function () {
        $.extend(true, netdataDashboard, customDashboard);
        callback();
    });
}

function initializeChartsAndCustomInfo() {
    NETDATA.alarms.callback = alarmsCallback;

    // download all the charts the server knows
    NETDATA.chartRegistry.downloadAll(initializeConfig.url, function (data) {
        if (data !== null) {
            if (initializeConfig.custom_info === true && typeof data.custom_info !== 'undefined' && data.custom_info !== "" && netdataSnapshotData === null) {
                //console.log('loading custom dashboard decorations from server ' + initializeConfig.url);
                loadCustomDashboardInfo(NETDATA.serverDefault + data.custom_info, function () {
                    initializeDynamicDashboardWithData(data);
                });
            } else {
                //console.log('not loading custom dashboard decorations from server ' + initializeConfig.url);
                initializeDynamicDashboardWithData(data);
            }
        }
    });
}

function xssModalDisableXss() {
    //console.log('disabling xss checks');
    NETDATA.xss.enabled = false;
    NETDATA.xss.enabled_for_data = false;
    initializeConfig.custom_info = true;
    initializeChartsAndCustomInfo();
    return false;
}

function xssModalKeepXss() {
    //console.log('keeping xss checks');
    NETDATA.xss.enabled = true;
    NETDATA.xss.enabled_for_data = true;
    initializeConfig.custom_info = false;
    initializeChartsAndCustomInfo();
    return false;
}

function initializeDynamicDashboard(netdata_url) {
    if (typeof netdata_url === 'undefined' || netdata_url === null) {
        netdata_url = NETDATA.serverDefault;
    }

    initializeConfig.url = netdata_url;

    // initialize clickable alarms
    NETDATA.alarms.chart_div_offset = -50;
    NETDATA.alarms.chart_div_id_prefix = 'chart_';
    NETDATA.alarms.chart_div_animation_duration = 0;

    NETDATA.pause(function () {
        if (typeof netdataCheckXSS !== 'undefined' && netdataCheckXSS === true) {
            //$("#loadOverlay").css("display","none");
            document.getElementById('netdataXssModalServer').innerText = initializeConfig.url;
            $('#xssModal').modal('show');
        } else {
            initializeChartsAndCustomInfo();
        }
    });
}

// ----------------------------------------------------------------------------

function versionLog(msg) {
    document.getElementById('versionCheckLog').innerHTML = msg;
}

// New way of checking for updates, based only on versions

function versionsMatch(v1, v2) {
    if (v1 == v2) {
        return true;
    } else {
        let s1 = v1.split('.');
        let s2 = v2.split('.');
        // Check major version
        let n1 = parseInt(s1[0].substring(1, 2), 10);
        let n2 = parseInt(s2[0].substring(1, 2), 10);
        if (n1 < n2) return false;
        else if (n1 > n2) return true;

        // Check minor version
        n1 = parseInt(s1[1], 10);
        n2 = parseInt(s2[1], 10);
        if (n1 < n2) return false;
        else if (n1 > n2) return true;

        // Split patch: format could be e.g. 0-22-nightly
        s1 = s1[2].split('-');
        s2 = s2[2].split('-');

        n1 = parseInt(s1[0], 10);
        n2 = parseInt(s2[0], 10);
        if (n1 < n2) return false;
        else if (n1 > n2) return true;

        n1 = (s1.length > 1) ? parseInt(s1[1], 10) : 0;
        n2 = (s2.length > 1) ? parseInt(s2[1], 10) : 0;
        if (n1 < n2) return false;
        else return true;
    }
}

function getGithubLatestVersion(callback) {
    versionLog('Downloading latest version id from github...');

    $.ajax({
        url: 'https://api.github.com/repos/netdata/netdata/releases/latest',
        async: true,
        cache: false
    })
        .done(function (data) {
            data = data.tag_name.replace(/(\r\n|\n|\r| |\t)/gm, "");
            versionLog('Latest stable version from github is ' + data);
            callback(data);
        })
        .fail(function () {
            versionLog('Failed to download the latest stable version id from github!');
            callback(null);
        });
}

function getGCSLatestVersion(callback) {
    versionLog('Downloading latest version id from GCS...');
    $.ajax({
        url: "https://www.googleapis.com/storage/v1/b/netdata-nightlies/o/latest-version.txt",
        async: true,
        cache: false
    })
        .done(function (response) {
            $.ajax({
                url: response.mediaLink,
                async: true,
                cache: false
            })
                .done(function (data) {
                    data = data.replace(/(\r\n|\n|\r| |\t)/gm, "");
                    versionLog('Latest nightly version from GCS is ' + data);
                    callback(data);
                })
                .fail(function (xhr, textStatus, errorThrown) {
                    versionLog('Failed to download the latest nightly version id from GCS!');
                    callback(null);
                });
        })
        .fail(function (xhr, textStatus, errorThrown) {
            versionLog('Failed to download the latest nightly version from GCS!');
            callback(null);
        });
}


function checkForUpdateByVersion(force, callback) {
    if (options.release_channel === 'stable') {
        getGithubLatestVersion(function (sha2) {
            callback(options.version, sha2);
        });
    } else {
        getGCSLatestVersion(function (sha2) {
            callback(options.version, sha2);
        });
    }
    return null;
}

function notifyForUpdate(force) {
    versionLog('<p>checking for updates...</p>');

    var now = Date.now();

    if (typeof force === 'undefined' || force !== true) {
        var last = loadLocalStorage('last_update_check');

        if (typeof last === 'string') {
            last = parseInt(last);
        } else {
            last = 0;
        }

        if (now - last < 3600000 * 8) {
            // no need to check it - too soon
            return;
        }
    }

    checkForUpdateByVersion(force, function (sha1, sha2) {
        var save = false;

        if (sha1 === null) {
            save = false;
            versionLog('<p><big>Failed to get your netdata version!</big></p><p>You can always get the latest netdata from <a href="https://github.com/netdata/netdata" target="_blank">its github page</a>.</p>');
        } else if (sha2 === null) {
            save = false;
            versionLog('<p><big>Failed to get the latest netdata version.</big></p><p>You can always get the latest netdata from <a href="https://github.com/netdata/netdata" target="_blank">its github page</a>.</p>');
        } else if (versionsMatch(sha1, sha2)) {
            save = true;
            versionLog('<p><big>You already have the latest netdata!</big></p><p>No update yet?<br/>We probably need some motivation to keep going on!</p><p>If you haven\'t already, <a href="https://github.com/netdata/netdata" target="_blank">give netdata a <b><i class="fas fa-star"></i></b> at its github page</a>.</p>');
        } else {
            save = true;
            var compare = 'https://learn.netdata.cloud/docs/agent/changelog/';
            versionLog('<p><big><strong>New version of netdata available!</strong></big></p><p>Latest version: <b><code>' + sha2 + '</code></b></p><p><a href="' + compare + '" target="_blank">Click here for the changes log</a> and<br/><a href="https://github.com/netdata/netdata/tree/master/packaging/installer/UPDATE.md" target="_blank">click here for directions on updating</a> your netdata installation.</p><p>We suggest to review the changes log for new features you may be interested, or important bug fixes you may need.<br/>Keeping your netdata updated is generally a good idea.</p>');

            document.getElementById('update_badge').innerHTML = '!';
        }

        if (save) {
            saveLocalStorage('last_update_check', now.toString());
        }
    });
}

// ----------------------------------------------------------------------------
// printing dashboards

function showPageFooter() {
    document.getElementById('footer').style.display = 'block';
}

function printPreflight() {
    var url = document.location.origin.toString() + document.location.pathname.toString() + document.location.search.toString() + urlOptions.genHash() + ';mode=print';
    var width = 990;
    var height = screen.height * 90 / 100;
    //console.log(url);
    //console.log(document.location);
    window.open(url, '', 'width=' + width.toString() + ',height=' + height.toString() + ',menubar=no,toolbar=no,personalbar=no,location=no,resizable=no,scrollbars=yes,status=no,chrome=yes,centerscreen=yes,attention=yes,dialog=yes');
    $('#printPreflightModal').modal('hide');
}

function printPage() {
    var print_is_rendering = true;

    $('#printModal').on('hide.bs.modal', function (e) {
        if (print_is_rendering === true) {
            e.preventDefault();
            return false;
        }

        return true;
    });

    $('#printModal').on('show.bs.modal', function () {
        var print_options = {
            stop_updates_when_focus_is_lost: false,
            update_only_visible: false,
            sync_selection: false,
            eliminate_zero_dimensions: false,
            pan_and_zoom_data_padding: false,
            show_help: false,
            legend_toolbox: false,
            resize_charts: false,
            pixels_per_point: 1
        };

        var x;
        for (x in print_options) {
            if (print_options.hasOwnProperty(x)) {
                NETDATA.options.current[x] = print_options[x];
            }
        }

        NETDATA.parseDom();
        showPageFooter();

        NETDATA.globalSelectionSync.stop();
        NETDATA.globalPanAndZoom.setMaster(NETDATA.options.targets[0], urlOptions.after, urlOptions.before);
        // NETDATA.onresize();

        var el = document.getElementById('printModalProgressBar');
        var eltxt = document.getElementById('printModalProgressBarText');

        function update_chart(idx) {
            var state = NETDATA.options.targets[--idx];

            var pcent = (NETDATA.options.targets.length - idx) * 100 / NETDATA.options.targets.length;
            $(el).css('width', pcent + '%').attr('aria-valuenow', pcent);
            eltxt.innerText = Math.round(pcent).toString() + '%, ' + state.id;

            setTimeout(function () {
                state.updateChart(function () {
                    NETDATA.options.targets[idx].resizeForPrint();

                    if (idx > 0) {
                        update_chart(idx);
                    } else {
                        print_is_rendering = false;
                        $('#printModal').modal('hide');
                        window.print();
                        window.close();
                    }
                })
            }, 0);
        }

        print_is_rendering = true;
        update_chart(NETDATA.options.targets.length);
    });

    $('#printModal').modal('show');
}

// --------------------------------------------------------------------

function jsonStringifyFn(obj) {
    return JSON.stringify(obj, function (key, value) {
        return (typeof value === 'function') ? value.toString() : value;
    });
}

function jsonParseFn(str) {
    return JSON.parse(str, function (key, value) {
        if (typeof value != 'string') {
            return value;
        }
        return (value.substring(0, 8) == 'function') ? eval('(' + value + ')') : value;
    });
}

// --------------------------------------------------------------------

var snapshotOptions = {
    bytes_per_chart: 2048,
    compressionDefault: 'pako.deflate.base64',

    compressions: {
        'none': {
            bytes_per_point_memory: 5.2,
            bytes_per_point_disk: 5.6,

            compress: function (s) {
                return s;
            },

            compressed_length: function (s) {
                return s.length;
            },

            uncompress: function (s) {
                return s;
            }
        },

        'pako.deflate.base64': {
            bytes_per_point_memory: 1.8,
            bytes_per_point_disk: 1.9,

            compress: function (s) {
                return btoa(pako.deflate(s, { to: 'string' }));
            },

            compressed_length: function (s) {
                return s.length;
            },

            uncompress: function (s) {
                return pako.inflate(atob(s), { to: 'string' });
            }
        },

        'pako.deflate': {
            bytes_per_point_memory: 1.4,
            bytes_per_point_disk: 3.2,

            compress: function (s) {
                return pako.deflate(s, { to: 'string' });
            },

            compressed_length: function (s) {
                return s.length;
            },

            uncompress: function (s) {
                return pako.inflate(s, { to: 'string' });
            }
        },

        'lzstring.utf16': {
            bytes_per_point_memory: 1.7,
            bytes_per_point_disk: 2.6,

            compress: function (s) {
                return LZString.compressToUTF16(s);
            },

            compressed_length: function (s) {
                return s.length * 2;
            },

            uncompress: function (s) {
                return LZString.decompressFromUTF16(s);
            }
        },

        'lzstring.base64': {
            bytes_per_point_memory: 2.1,
            bytes_per_point_disk: 2.3,

            compress: function (s) {
                return LZString.compressToBase64(s);
            },

            compressed_length: function (s) {
                return s.length;
            },

            uncompress: function (s) {
                return LZString.decompressFromBase64(s);
            }
        },

        'lzstring.uri': {
            bytes_per_point_memory: 2.1,
            bytes_per_point_disk: 2.3,

            compress: function (s) {
                return LZString.compressToEncodedURIComponent(s);
            },

            compressed_length: function (s) {
                return s.length;
            },

            uncompress: function (s) {
                return LZString.decompressFromEncodedURIComponent(s);
            }
        }
    }
};

// --------------------------------------------------------------------
// loading snapshots

function loadSnapshotModalLog(priority, msg) {
    document.getElementById('loadSnapshotStatus').className = "alert alert-" + priority;
    document.getElementById('loadSnapshotStatus').innerHTML = msg;
}

var tmpSnapshotData = null;

function loadSnapshot() {
    $('#loadSnapshotImport').addClass('disabled');

    if (tmpSnapshotData === null) {
        loadSnapshotPreflightEmpty();
        loadSnapshotModalLog('danger', 'no data have been loaded');
        return;
    }

    loadPako(function () {
        loadLzString(function () {
            loadSnapshotModalLog('info', 'Please wait, activating snapshot...');
            $('#loadSnapshotModal').modal('hide');

            netdataShowAlarms = false;
            netdataRegistry = false;
            netdataServer = tmpSnapshotData.server;
            NETDATA.serverDefault = netdataServer;

            document.getElementById('charts_div').innerHTML = '';
            document.getElementById('sidebar').innerHTML = '';
            NETDATA.globalReset();

            if (typeof tmpSnapshotData.hash !== 'undefined') {
                urlOptions.hash = tmpSnapshotData.hash;
            } else {
                urlOptions.hash = '#';
            }

            if (typeof tmpSnapshotData.info !== 'undefined') {
                var info = jsonParseFn(tmpSnapshotData.info);
                if (typeof info.menu !== 'undefined') {
                    netdataDashboard.menu = info.menu;
                }

                if (typeof info.submenu !== 'undefined') {
                    netdataDashboard.submenu = info.submenu;
                }

                if (typeof info.context !== 'undefined') {
                    netdataDashboard.context = info.context;
                }
            }

            if (typeof tmpSnapshotData.compression !== 'string') {
                tmpSnapshotData.compression = 'none';
            }

            if (typeof snapshotOptions.compressions[tmpSnapshotData.compression] === 'undefined') {
                alert('unknown compression method: ' + tmpSnapshotData.compression);
                tmpSnapshotData.compression = 'none';
            }

            tmpSnapshotData.uncompress = snapshotOptions.compressions[tmpSnapshotData.compression].uncompress;
            netdataSnapshotData = tmpSnapshotData;

            urlOptions.after = tmpSnapshotData.after_ms;
            urlOptions.before = tmpSnapshotData.before_ms;

            if (typeof tmpSnapshotData.highlight_after_ms !== 'undefined'
                && tmpSnapshotData.highlight_after_ms !== null
                && tmpSnapshotData.highlight_after_ms > 0
                && typeof tmpSnapshotData.highlight_before_ms !== 'undefined'
                && tmpSnapshotData.highlight_before_ms !== null
                && tmpSnapshotData.highlight_before_ms > 0
            ) {
                urlOptions.highlight_after = tmpSnapshotData.highlight_after_ms;
                urlOptions.highlight_before = tmpSnapshotData.highlight_before_ms;
                urlOptions.highlight = true;
            } else {
                urlOptions.highlight_after = 0;
                urlOptions.highlight_before = 0;
                urlOptions.highlight = false;
            }

            netdataCheckXSS = false; // disable the modal - this does not affect XSS checks, since dashboard.js is already loaded
            NETDATA.xss.enabled = true;             // we should not do any remote requests, but if we do, check them
            NETDATA.xss.enabled_for_data = true;    // check also snapshot data - that have been excluded from the initial check, due to compression
            loadSnapshotPreflightEmpty();
            initializeDynamicDashboard();
        });
    });
};

function loadSnapshotPreflightFile(file) {
    var filename = NETDATA.xss.string(file.name);
    var fr = new FileReader();
    fr.onload = function (e) {
        document.getElementById('loadSnapshotFilename').innerHTML = filename;
        var result = null;
        try {
            result = NETDATA.xss.checkAlways('snapshot', JSON.parse(e.target.result), /^(snapshot\.info|snapshot\.data)$/);

            //console.log(result);
            var date_after = new Date(result.after_ms);
            var date_before = new Date(result.before_ms);

            if (typeof result.charts_ok === 'undefined') {
                result.charts_ok = 'unknown';
            }

            if (typeof result.charts_failed === 'undefined') {
                result.charts_failed = 0;
            }

            if (typeof result.compression === 'undefined') {
                result.compression = 'none';
            }

            if (typeof result.data_size === 'undefined') {
                result.data_size = 0;
            }

            document.getElementById('loadSnapshotFilename').innerHTML = '<code>' + filename + '</code>';
            document.getElementById('loadSnapshotHostname').innerHTML = '<b>' + result.hostname + '</b>, netdata version: <b>' + result.netdata_version.toString() + '</b>';
            document.getElementById('loadSnapshotURL').innerHTML = result.url;
            document.getElementById('loadSnapshotCharts').innerHTML = result.charts.charts_count.toString() + ' charts, ' + result.charts.dimensions_count.toString() + ' dimensions, ' + result.data_points.toString() + ' points per dimension, ' + Math.round(result.duration_ms / result.data_points).toString() + ' ms per point';
            document.getElementById('loadSnapshotInfo').innerHTML = 'version: <b>' + result.snapshot_version.toString() + '</b>, includes <b>' + result.charts_ok.toString() + '</b> unique chart data queries ' + ((result.charts_failed > 0) ? ('<b>' + result.charts_failed.toString() + '</b> failed') : '').toString() + ', compressed with <code>' + result.compression.toString() + '</code>, data size ' + (Math.round(result.data_size * 100 / 1024 / 1024) / 100).toString() + ' MB';
            document.getElementById('loadSnapshotTimeRange').innerHTML = '<b>' + NETDATA.dateTime.localeDateString(date_after) + ' ' + NETDATA.dateTime.localeTimeString(date_after) + '</b> to <b>' + NETDATA.dateTime.localeDateString(date_before) + ' ' + NETDATA.dateTime.localeTimeString(date_before) + '</b>';
            document.getElementById('loadSnapshotComments').innerHTML = ((result.comments) ? result.comments : '').toString();
            loadSnapshotModalLog('success', 'File loaded, click <b>Import</b> to render it!');
            $('#loadSnapshotImport').removeClass('disabled');

            tmpSnapshotData = result;
        }
        catch (e) {
            console.log(e);
            document.getElementById('loadSnapshotStatus').className = "alert alert-danger";
            document.getElementById('loadSnapshotStatus').innerHTML = "Failed to parse this file!";
            $('#loadSnapshotImport').addClass('disabled');
        }
    }

    //console.log(file);
    fr.readAsText(file);
};

function loadSnapshotPreflightEmpty() {
    document.getElementById('loadSnapshotFilename').innerHTML = '';
    document.getElementById('loadSnapshotHostname').innerHTML = '';
    document.getElementById('loadSnapshotURL').innerHTML = '';
    document.getElementById('loadSnapshotCharts').innerHTML = '';
    document.getElementById('loadSnapshotInfo').innerHTML = '';
    document.getElementById('loadSnapshotTimeRange').innerHTML = '';
    document.getElementById('loadSnapshotComments').innerHTML = '';
    loadSnapshotModalLog('success', 'Browse for a snapshot file (or drag it and drop it here), then click <b>Import</b> to render it.');
    $('#loadSnapshotImport').addClass('disabled');
};

var loadSnapshotDragAndDropInitialized = false;

function loadSnapshotDragAndDropSetup() {
    if (loadSnapshotDragAndDropInitialized === false) {
        loadSnapshotDragAndDropInitialized = true;
        $('#loadSnapshotDragAndDrop')
            .on('drag dragstart dragend dragover dragenter dragleave drop', function (e) {
                e.preventDefault();
                e.stopPropagation();
            })
            .on('drop', function (e) {
                if (e.originalEvent.dataTransfer.files.length) {
                    loadSnapshotPreflightFile(e.originalEvent.dataTransfer.files.item(0));
                } else {
                    loadSnapshotPreflightEmpty();
                    loadSnapshotModalLog('danger', 'No file selected');
                }
            });
    }
};

function loadSnapshotPreflight() {
    var files = document.getElementById('loadSnapshotSelectFiles').files;
    if (files.length <= 0) {
        loadSnapshotPreflightEmpty();
        loadSnapshotModalLog('danger', 'No file selected');
        return;
    }

    loadSnapshotModalLog('info', 'Loading file...');

    loadSnapshotPreflightFile(files.item(0));
}

// --------------------------------------------------------------------
// saving snapshots

var saveSnapshotStop = false;

function saveSnapshotCancel() {
    saveSnapshotStop = true;
}

var saveSnapshotModalInitialized = false;

function saveSnapshotModalSetup() {
    if (saveSnapshotModalInitialized === false) {
        saveSnapshotModalInitialized = true;
        $('#saveSnapshotModal')
            .on('hide.bs.modal', saveSnapshotCancel)
            .on('show.bs.modal', saveSnapshotModalInit)
            .on('shown.bs.modal', function () {
                $('#saveSnapshotResolutionSlider').find(".slider-handle:first").attr("tabindex", 1);
                document.getElementById('saveSnapshotComments').focus();
            });
    }
};

function saveSnapshotModalLog(priority, msg) {
    document.getElementById('saveSnapshotStatus').className = "alert alert-" + priority;
    document.getElementById('saveSnapshotStatus').innerHTML = msg;
}

function saveSnapshotModalShowExpectedSize() {
    var points = Math.round(saveSnapshotViewDuration / saveSnapshotSelectedSecondsPerPoint);
    var priority = 'info';
    var msg = 'A moderate snapshot.';

    var sizemb = Math.round(
        (options.data.charts_count * snapshotOptions.bytes_per_chart
            + options.data.dimensions_count * points * snapshotOptions.compressions[saveSnapshotCompression].bytes_per_point_disk)
        * 10 / 1024 / 1024) / 10;

    var memmb = Math.round(
        (options.data.charts_count * snapshotOptions.bytes_per_chart
            + options.data.dimensions_count * points * snapshotOptions.compressions[saveSnapshotCompression].bytes_per_point_memory)
        * 10 / 1024 / 1024) / 10;

    if (sizemb < 10) {
        priority = 'success';
        msg = 'A nice small snapshot!';
    }
    if (sizemb > 50) {
        priority = 'warning';
        msg = 'Will stress your browser...';
    }
    if (sizemb > 100) {
        priority = 'danger';
        msg = 'Hm... good luck...';
    }

    saveSnapshotModalLog(priority, 'The snapshot will have ' + points.toString() + ' points per dimension. Expected size on disk ' + sizemb + ' MB, at browser memory ' + memmb + ' MB.<br/>' + msg);
}

var saveSnapshotCompression = snapshotOptions.compressionDefault;

function saveSnapshotSetCompression(name) {
    saveSnapshotCompression = name;
    document.getElementById('saveSnapshotCompressionName').innerHTML = saveSnapshotCompression;
    saveSnapshotModalShowExpectedSize();
}

var saveSnapshotSlider = null;
var saveSnapshotSelectedSecondsPerPoint = 1;
var saveSnapshotViewDuration = 1;

function saveSnapshotModalInit() {
    $('#saveSnapshotModalProgressSection').hide();
    $('#saveSnapshotResolutionRadio').show();
    saveSnapshotModalLog('info', 'Select resolution and click <b>Save</b>');
    $('#saveSnapshotExport').removeClass('disabled');

    loadBootstrapSlider(function () {
        saveSnapshotViewDuration = options.duration;
        var start_ms = Math.round(Date.now() - saveSnapshotViewDuration * 1000);

        if (NETDATA.globalPanAndZoom.isActive() === true) {
            saveSnapshotViewDuration = Math.round((NETDATA.globalPanAndZoom.force_before_ms - NETDATA.globalPanAndZoom.force_after_ms) / 1000);
            start_ms = NETDATA.globalPanAndZoom.force_after_ms;
        }

        var start_date = new Date(start_ms);
        var yyyymmddhhssmm = start_date.getFullYear() + NETDATA.zeropad(start_date.getMonth() + 1) + NETDATA.zeropad(start_date.getDate()) + '-' + NETDATA.zeropad(start_date.getHours()) + NETDATA.zeropad(start_date.getMinutes()) + NETDATA.zeropad(start_date.getSeconds());

        document.getElementById('saveSnapshotFilename').value = 'netdata-' + options.hostname.toString() + '-' + yyyymmddhhssmm.toString() + '-' + saveSnapshotViewDuration.toString() + '.snapshot';
        saveSnapshotSetCompression(saveSnapshotCompression);

        var min = options.update_every;
        var max = Math.round(saveSnapshotViewDuration / 100);

        if (NETDATA.globalPanAndZoom.isActive() === false) {
            max = Math.round(saveSnapshotViewDuration / 50);
        }

        var view = Math.round(saveSnapshotViewDuration / Math.round($(document.getElementById('charts_div')).width() / 2));

        // console.log('view duration: ' + saveSnapshotViewDuration + ', min: ' + min + ', max: ' + max + ', view: ' + view);

        if (max < 10) {
            max = 10;
        }
        if (max < min) {
            max = min;
        }
        if (view < min) {
            view = min;
        }
        if (view > max) {
            view = max;
        }

        if (saveSnapshotSlider !== null) {
            saveSnapshotSlider.destroy();
        }

        saveSnapshotSlider = new Slider('#saveSnapshotResolutionSlider', {
            ticks: [min, view, max],
            min: min,
            max: max,
            step: options.update_every,
            value: view,
            scale: (max > 100) ? 'logarithmic' : 'linear',
            tooltip: 'always',
            formatter: function (value) {
                if (value < 1) {
                    value = 1;
                }

                if (value < options.data.update_every) {
                    value = options.data.update_every;
                }

                saveSnapshotSelectedSecondsPerPoint = value;
                saveSnapshotModalShowExpectedSize();

                var seconds = ' seconds ';
                if (value === 1) {
                    seconds = ' second ';
                }

                return value + seconds + 'per point' + ((value === options.data.update_every) ? ', server default' : '').toString();
            }
        });
    });
}

function saveSnapshot() {
    loadPako(function () {
        loadLzString(function () {
            saveSnapshotStop = false;
            $('#saveSnapshotModalProgressSection').show();
            $('#saveSnapshotResolutionRadio').hide();
            $('#saveSnapshotExport').addClass('disabled');

            var filename = document.getElementById('saveSnapshotFilename').value;
            // console.log(filename);
            saveSnapshotModalLog('info', 'Generating snapshot as <code>' + filename.toString() + '</code>');

            var save_options = {
                stop_updates_when_focus_is_lost: false,
                update_only_visible: false,
                sync_selection: false,
                eliminate_zero_dimensions: true,
                pan_and_zoom_data_padding: false,
                show_help: false,
                legend_toolbox: false,
                resize_charts: false,
                pixels_per_point: 1
            };
            var backedup_options = {};

            var x;
            for (x in save_options) {
                if (save_options.hasOwnProperty(x)) {
                    backedup_options[x] = NETDATA.options.current[x];
                    NETDATA.options.current[x] = save_options[x];
                }
            }

            var el = document.getElementById('saveSnapshotModalProgressBar');
            var eltxt = document.getElementById('saveSnapshotModalProgressBarText');

            options.data.charts_by_name = null;

            var saveData = {
                hostname: options.hostname,
                server: NETDATA.serverDefault,
                netdata_version: options.data.version,
                snapshot_version: 1,
                after_ms: Date.now() - options.duration * 1000,
                before_ms: Date.now(),
                highlight_after_ms: urlOptions.highlight_after,
                highlight_before_ms: urlOptions.highlight_before,
                duration_ms: options.duration * 1000,
                update_every_ms: options.update_every * 1000,
                data_points: 0,
                url: ((urlOptions.server !== null) ? urlOptions.server : document.location.origin.toString() + document.location.pathname.toString() + document.location.search.toString()).toString(),
                comments: document.getElementById('saveSnapshotComments').value.toString(),
                hash: urlOptions.hash,
                charts: options.data,
                info: jsonStringifyFn({
                    menu: netdataDashboard.menu,
                    submenu: netdataDashboard.submenu,
                    context: netdataDashboard.context
                }),
                charts_ok: 0,
                charts_failed: 0,
                compression: saveSnapshotCompression,
                data_size: 0,
                data: {}
            };

            if (typeof snapshotOptions.compressions[saveData.compression] === 'undefined') {
                alert('unknown compression method: ' + saveData.compression);
                saveData.compression = 'none';
            }

            var compress = snapshotOptions.compressions[saveData.compression].compress;
            var compressed_length = snapshotOptions.compressions[saveData.compression].compressed_length;

            function pack_api1_v1_chart_data(state) {
                if (state.library_name === null || state.data === null) {
                    return;
                }

                var data = state.data;
                state.data = null;
                data.state = null;
                var str = JSON.stringify(data);

                if (typeof str === 'string') {
                    var cstr = compress(str);
                    saveData.data[state.chartDataUniqueID()] = cstr;
                    return compressed_length(cstr);
                } else {
                    return 0;
                }
            }

            var clearPanAndZoom = false;
            if (NETDATA.globalPanAndZoom.isActive() === false) {
                NETDATA.globalPanAndZoom.setMaster(NETDATA.options.targets[0], saveData.after_ms, saveData.before_ms);
                clearPanAndZoom = true;
            }

            saveData.after_ms = NETDATA.globalPanAndZoom.force_after_ms;
            saveData.before_ms = NETDATA.globalPanAndZoom.force_before_ms;
            saveData.duration_ms = saveData.before_ms - saveData.after_ms;
            saveData.data_points = Math.round((saveData.before_ms - saveData.after_ms) / (saveSnapshotSelectedSecondsPerPoint * 1000));
            saveSnapshotModalLog('info', 'Generating snapshot with ' + saveData.data_points.toString() + ' data points per dimension...');

            var charts_count = 0;
            var charts_ok = 0;
            var charts_failed = 0;

            function saveSnapshotRestore() {
                $('#saveSnapshotModal').modal('hide');

                // restore the options
                var x;
                for (x in backedup_options) {
                    if (backedup_options.hasOwnProperty(x)) {
                        NETDATA.options.current[x] = backedup_options[x];
                    }
                }

                $(el).css('width', '0%').attr('aria-valuenow', 0);
                eltxt.innerText = '0%';

                if (clearPanAndZoom) {
                    NETDATA.globalPanAndZoom.clearMaster();
                }

                NETDATA.options.force_data_points = 0;
                NETDATA.options.fake_chart_rendering = false;
                NETDATA.onscroll_updater_enabled = true;
                NETDATA.onresize();
                NETDATA.unpause();

                $('#saveSnapshotExport').removeClass('disabled');
            }

            NETDATA.globalSelectionSync.stop();
            NETDATA.options.force_data_points = saveData.data_points;
            NETDATA.options.fake_chart_rendering = true;
            NETDATA.onscroll_updater_enabled = false;
            NETDATA.abortAllRefreshes();

            var size = 0;
            var info = ' Resolution: <b>' + saveSnapshotSelectedSecondsPerPoint.toString() + ((saveSnapshotSelectedSecondsPerPoint === 1) ? ' second ' : ' seconds ').toString() + 'per point</b>.';

            function update_chart(idx) {
                if (saveSnapshotStop === true) {
                    saveSnapshotModalLog('info', 'Cancelled!');
                    saveSnapshotRestore();
                    return;
                }

                var state = NETDATA.options.targets[--idx];

                var pcent = (NETDATA.options.targets.length - idx) * 100 / NETDATA.options.targets.length;
                $(el).css('width', pcent + '%').attr('aria-valuenow', pcent);
                eltxt.innerText = Math.round(pcent).toString() + '%, ' + state.id;

                setTimeout(function () {
                    charts_count++;
                    state.isVisible(true);
                    state.current.force_after_ms = saveData.after_ms;
                    state.current.force_before_ms = saveData.before_ms;

                    state.updateChart(function (status, reason) {
                        state.current.force_after_ms = null;
                        state.current.force_before_ms = null;

                        if (status === true) {
                            charts_ok++;
                            // state.log('ok');
                            size += pack_api1_v1_chart_data(state);
                        } else {
                            charts_failed++;
                            state.log('failed to be updated: ' + reason);
                        }

                        saveSnapshotModalLog((charts_failed) ? 'danger' : 'info', 'Generated snapshot data size <b>' + (Math.round(size * 100 / 1024 / 1024) / 100).toString() + ' MB</b>. ' + ((charts_failed) ? (charts_failed.toString() + ' charts have failed to be downloaded') : '').toString() + info);

                        if (idx > 0) {
                            update_chart(idx);
                        } else {
                            saveData.charts_ok = charts_ok;
                            saveData.charts_failed = charts_failed;
                            saveData.data_size = size;
                            // console.log(saveData.compression + ': ' + (size / (options.data.dimensions_count * Math.round(saveSnapshotViewDuration / saveSnapshotSelectedSecondsPerPoint))).toString());

                            // save it
                            // console.log(saveData);
                            saveObjectToClient(saveData, filename);

                            if (charts_failed > 0) {
                                alert(charts_failed.toString() + ' failed to be downloaded');
                            }

                            saveSnapshotRestore();
                            saveData = null;
                        }
                    })
                }, 0);
            }

            update_chart(NETDATA.options.targets.length);
        });
    });
}

// --------------------------------------------------------------------
// activate netdata on the page

function dashboardSettingsSetup() {
    var update_options_modal = function () {
        // console.log('update_options_modal');

        var sync_option = function (option) {
            var self = $('#' + option);

            if (self.prop('checked') !== NETDATA.getOption(option)) {
                // console.log('switching ' + option.toString());
                self.bootstrapToggle(NETDATA.getOption(option) ? 'on' : 'off');
            }
        };

        var theme_sync_option = function (option) {
            var self = $('#' + option);

            self.bootstrapToggle(netdataTheme === 'slate' ? 'on' : 'off');
        };
        var units_sync_option = function (option) {
            var self = $('#' + option);

            if (self.prop('checked') !== (NETDATA.getOption('units') === 'auto')) {
                self.bootstrapToggle(NETDATA.getOption('units') === 'auto' ? 'on' : 'off');
            }

            if (self.prop('checked') === true) {
                $('#settingsLocaleTempRow').show();
                $('#settingsLocaleTimeRow').show();
            } else {
                $('#settingsLocaleTempRow').hide();
                $('#settingsLocaleTimeRow').hide();
            }
        };
        var temp_sync_option = function (option) {
            var self = $('#' + option);

            if (self.prop('checked') !== (NETDATA.getOption('temperature') === 'celsius')) {
                self.bootstrapToggle(NETDATA.getOption('temperature') === 'celsius' ? 'on' : 'off');
            }
        };
        var timezone_sync_option = function (option) {
            var self = $('#' + option);

            document.getElementById('browser_timezone').innerText = NETDATA.options.browser_timezone;
            document.getElementById('server_timezone').innerText = NETDATA.options.server_timezone;
            document.getElementById('current_timezone').innerText = (NETDATA.options.current.timezone === 'default') ? 'unset, using browser default' : NETDATA.options.current.timezone;

            if (self.prop('checked') === NETDATA.dateTime.using_timezone) {
                self.bootstrapToggle(NETDATA.dateTime.using_timezone ? 'off' : 'on');
            }
        };

        sync_option('eliminate_zero_dimensions');
        sync_option('destroy_on_hide');
        sync_option('async_on_scroll');
        sync_option('parallel_refresher');
        sync_option('concurrent_refreshes');
        sync_option('sync_selection');
        sync_option('sync_pan_and_zoom');
        sync_option('stop_updates_when_focus_is_lost');
        sync_option('smooth_plot');
        sync_option('pan_and_zoom_data_padding');
        sync_option('show_help');
        sync_option('seconds_as_time');
        theme_sync_option('netdata_theme_control');
        units_sync_option('units_conversion');
        temp_sync_option('units_temp');
        timezone_sync_option('local_timezone');

        if (NETDATA.getOption('parallel_refresher') === false) {
            $('#concurrent_refreshes_row').hide();
        } else {
            $('#concurrent_refreshes_row').show();
        }
    };
    NETDATA.setOption('setOptionCallback', update_options_modal);

    // handle options changes
    $('#eliminate_zero_dimensions').change(function () {
        NETDATA.setOption('eliminate_zero_dimensions', $(this).prop('checked'));
    });
    $('#destroy_on_hide').change(function () {
        NETDATA.setOption('destroy_on_hide', $(this).prop('checked'));
    });
    $('#async_on_scroll').change(function () {
        NETDATA.setOption('async_on_scroll', $(this).prop('checked'));
    });
    $('#parallel_refresher').change(function () {
        NETDATA.setOption('parallel_refresher', $(this).prop('checked'));
    });
    $('#concurrent_refreshes').change(function () {
        NETDATA.setOption('concurrent_refreshes', $(this).prop('checked'));
    });
    $('#sync_selection').change(function () {
        NETDATA.setOption('sync_selection', $(this).prop('checked'));
    });
    $('#sync_pan_and_zoom').change(function () {
        NETDATA.setOption('sync_pan_and_zoom', $(this).prop('checked'));
    });
    $('#stop_updates_when_focus_is_lost').change(function () {
        urlOptions.update_always = !$(this).prop('checked');
        urlOptions.hashUpdate();

        NETDATA.setOption('stop_updates_when_focus_is_lost', !urlOptions.update_always);
    });
    $('#smooth_plot').change(function () {
        NETDATA.setOption('smooth_plot', $(this).prop('checked'));
    });
    $('#pan_and_zoom_data_padding').change(function () {
        NETDATA.setOption('pan_and_zoom_data_padding', $(this).prop('checked'));
    });
    $('#seconds_as_time').change(function () {
        NETDATA.setOption('seconds_as_time', $(this).prop('checked'));
    });
    $('#local_timezone').change(function () {
        if ($(this).prop('checked')) {
            selected_server_timezone('default', true);
        } else {
            selected_server_timezone('default', false);
        }
    });

    $('#units_conversion').change(function () {
        NETDATA.setOption('units', $(this).prop('checked') ? 'auto' : 'original');
    });
    $('#units_temp').change(function () {
        NETDATA.setOption('temperature', $(this).prop('checked') ? 'celsius' : 'fahrenheit');
    });

    $('#show_help').change(function () {
        urlOptions.help = $(this).prop('checked');
        urlOptions.hashUpdate();

        NETDATA.setOption('show_help', urlOptions.help);
        netdataReload();
    });

    // this has to be the last
    // it reloads the page
    $('#netdata_theme_control').change(function () {
        urlOptions.theme = $(this).prop('checked') ? 'slate' : 'white';
        urlOptions.hashUpdate();

        if (setTheme(urlOptions.theme)) {
            netdataReload();
        }
    });
}

function scrollDashboardTo() {
    if (netdataSnapshotData !== null && typeof netdataSnapshotData.hash !== 'undefined') {
        //console.log(netdataSnapshotData.hash);
        scrollToId(netdataSnapshotData.hash.replace('#', ''));
    } else {
        // check if we have to jump to a specific section
        scrollToId(urlOptions.hash.replace('#', ''));

        if (urlOptions.chart !== null) {
            NETDATA.alarms.scrollToChart(urlOptions.chart);
            //urlOptions.hash = '#' + NETDATA.name2id('menu_' + charts[c].menu + '_submenu_' + charts[c].submenu);
            //urlOptions.hash = '#chart_' + NETDATA.name2id(urlOptions.chart);
            //console.log('hash = ' + urlOptions.hash);
        }
    }
}

var modalHiddenCallback = null;

function scrollToChartAfterHidingModal(chart, alarmDate, alarmStatus) {
    modalHiddenCallback = function () {
        NETDATA.alarms.scrollToChart(chart, alarmDate);

        if (['WARNING', 'CRITICAL'].includes(alarmStatus)) {
            const currentChartState = NETDATA.options.targets.find(
              (chartState) => chartState.id === chart,
            )
            const twoMinutes = 2 * 60 * 1000
            NETDATA.globalPanAndZoom.setMaster(
              currentChartState,
              alarmDate - twoMinutes,
              alarmDate + twoMinutes,
            )
        }
    };
}

// ----------------------------------------------------------------------------

function enableTooltipsAndPopovers() {
    $('[data-toggle="tooltip"]').tooltip({
        animated: 'fade',
        trigger: 'hover',
        html: true,
        delay: { show: 500, hide: 0 },
        container: 'body'
    });
    $('[data-toggle="popover"]').popover();
}

// ----------------------------------------------------------------------------

var runOnceOnDashboardLastRun = 0;

function runOnceOnDashboardWithjQuery() {
    if (runOnceOnDashboardLastRun !== 0) {
        scrollDashboardTo();

        // restore the scrollspy at the proper position
        $(document.body).scrollspy('refresh');
        $(document.body).scrollspy('process');

        return;
    }

    runOnceOnDashboardLastRun = Date.now();

    // ------------------------------------------------------------------------
    // bootstrap modals

    // prevent bootstrap modals from scrolling the page
    // maintains the current scroll position
    // https://stackoverflow.com/a/34754029/4525767

    var scrollPos = 0;
    var modal_depth = 0;                            // how many modals are currently open
    var modal_shown = false;                        // set to true, if a modal is shown
    var netdata_paused_on_modal = false;            // set to true, if the modal paused netdata
    var scrollspyOffset = $(window).height() / 3;   // will be updated below - the offset of scrollspy to select an item

    $('.modal')
        .on('show.bs.modal', function () {
            if (modal_depth === 0) {
                scrollPos = window.scrollY;

                $('body').css({
                    overflow: 'hidden',
                    position: 'fixed',
                    top: -scrollPos
                });

                modal_shown = true;

                if (NETDATA.options.pauseCallback === null) {
                    NETDATA.pause(function () {
                    });
                    netdata_paused_on_modal = true;
                } else {
                    netdata_paused_on_modal = false;
                }
            }

            modal_depth++;
            //console.log(urlOptions.after);

        })
        .on('hide.bs.modal', function () {

            modal_depth--;

            if (modal_depth <= 0) {
                modal_depth = 0;

                $('body')
                    .css({
                        overflow: '',
                        position: '',
                        top: ''
                    });

                // scroll to the position we had open before the modal
                $('html, body')
                    .animate({ scrollTop: scrollPos }, 0);

                // unpause netdata, if we paused it
                if (netdata_paused_on_modal === true) {
                    NETDATA.unpause();
                    netdata_paused_on_modal = false;
                }

                // restore the scrollspy at the proper position
                $(document.body).scrollspy('process');
            }
            //console.log(urlOptions.after);
        })
        .on('hidden.bs.modal', function () {
            if (modal_depth === 0) {
                modal_shown = false;
            }

            if (typeof modalHiddenCallback === 'function') {
                modalHiddenCallback();
            }

            modalHiddenCallback = null;
            //console.log(urlOptions.after);
        });

    // ------------------------------------------------------------------------
    // sidebar / affix

    if (shouldShowSignInBanner()) {
        const el = document.getElementById("sign-in-banner");
        if (el) {
            el.style.display = "initial";
            el.classList.add(`theme-${netdataTheme}`);
        }
    }

    $('#sidebar')
        .affix({
            offset: {
                top: (isdemo()) ? 150 : 0,
                bottom: 0
            }
        })
        .on('affixed.bs.affix', function () {
            // fix scrolling of very long affix lists
            // http://stackoverflow.com/questions/21691585/bootstrap-3-1-0-affix-too-long

            $(this).removeAttr('style');
        })
        .on('affix-top.bs.affix', function () {
            // fix bootstrap affix click bug
            // https://stackoverflow.com/a/37847981/4525767

            if (modal_shown) {
                return false;
            }
        })
        .on('activate.bs.scrollspy', function (e) {
            // change the URL based on the current position of the screen

            if (modal_shown === false) {
                var el = $(e.target);
                var hash = el.find('a').attr('href');
                if (typeof hash === 'string' && hash.substring(0, 1) === '#' && urlOptions.hash.startsWith(hash + '_submenu_') === false) {
                    urlOptions.hash = hash;
                    urlOptions.hashUpdate();
                }
            }
        });

    Ps.initialize(document.getElementById('sidebar'), {
        wheelSpeed: 0.5,
        wheelPropagation: true,
        swipePropagation: true,
        minScrollbarLength: null,
        maxScrollbarLength: null,
        useBothWheelAxes: false,
        suppressScrollX: true,
        suppressScrollY: false,
        scrollXMarginOffset: 0,
        scrollYMarginOffset: 0,
        theme: 'default'
    });

    // ------------------------------------------------------------------------
    // scrollspy

    if (scrollspyOffset > 250) {
        scrollspyOffset = 250;
    }
    if (scrollspyOffset < 75) {
        scrollspyOffset = 75;
    }
    document.body.setAttribute('data-offset', scrollspyOffset);

    // scroll the dashboard, before activating the scrollspy, so that our
    // hash will not be updated before we got the chance to scroll to it
    scrollDashboardTo();

    $(document.body).scrollspy({
        target: '#sidebar',
        offset: scrollspyOffset // controls the diff of the <hX> element to the top, to select it
    });

    // ------------------------------------------------------------------------
    // my-netdata menu

    Ps.initialize(document.getElementById('my-netdata-dropdown-content'), {
        wheelSpeed: 1,
        wheelPropagation: false,
        swipePropagation: false,
        minScrollbarLength: null,
        maxScrollbarLength: null,
        useBothWheelAxes: false,
        suppressScrollX: true,
        suppressScrollY: false,
        scrollXMarginOffset: 0,
        scrollYMarginOffset: 0,
        theme: 'default'
    });

    $('#myNetdataDropdownParent')
        .on('show.bs.dropdown', function () {
            var hash = urlOptions.genHash();
            $('.registry_link').each(function (idx) {
                this.setAttribute('href', this.getAttribute("href").replace(/#.*$/, hash));
            });

            NETDATA.pause(function () {
            });
        })
        .on('shown.bs.dropdown', function () {
            Ps.update(document.getElementById('my-netdata-dropdown-content'));
            myNetdataMenuDidShow();
        })
        .on('hidden.bs.dropdown', function () {
            NETDATA.unpause();
        });

    $('#deleteRegistryModal')
        .on('hidden.bs.modal', function () {
            deleteRegistryGuid = null;
        });

    // ------------------------------------------------------------------------
    // update modal

    $('#updateModal')
        .on('show.bs.modal', function () {
            versionLog('checking, please wait...');
        })
        .on('shown.bs.modal', function () {
            notifyForUpdate(true);
        });

    // ------------------------------------------------------------------------
    // alarms modal

    $('#alarmsModal')
        .on('shown.bs.modal', function () {
            alarmsUpdateModal();
        })
        .on('hidden.bs.modal', function () {
            document.getElementById('alarms_active').innerHTML =
                document.getElementById('alarms_all').innerHTML =
                document.getElementById('alarms_log').innerHTML =
                'loading...';
        });

    // ------------------------------------------------------------------------

    dashboardSettingsSetup();
    loadSnapshotDragAndDropSetup();
    saveSnapshotModalSetup();
    showPageFooter();

    // ------------------------------------------------------------------------
    // https://github.com/viralpatel/jquery.shorten/blob/master/src/jquery.shorten.js

    $.fn.shorten = function (settings) {
        "use strict";

        var config = {
            showChars: 750,
            minHideChars: 10,
            ellipsesText: "...",
            moreText: '<i class="fas fa-expand"></i> show more information',
            lessText: '<i class="fas fa-compress"></i> show less information',
            onLess: function () {
                NETDATA.onscroll();
            },
            onMore: function () {
                NETDATA.onscroll();
            },
            errMsg: null,
            force: false
        };

        if (settings) {
            $.extend(config, settings);
        }

        if ($(this).data('jquery.shorten') && !config.force) {
            return false;
        }
        $(this).data('jquery.shorten', true);

        $(document).off("click", '.morelink');

        $(document).on({
            click: function () {

                var $this = $(this);
                if ($this.hasClass('less')) {
                    $this.removeClass('less');
                    $this.html(config.moreText);
                    $this.parent().prev().animate({ 'height': '0' + '%' }, 0, function () {
                        $this.parent().prev().prev().show();
                    }).hide(0, function () {
                        config.onLess();
                    });
                } else {
                    $this.addClass('less');
                    $this.html(config.lessText);
                    $this.parent().prev().animate({ 'height': '100' + '%' }, 0, function () {
                        $this.parent().prev().prev().hide();
                    }).show(0, function () {
                        config.onMore();
                    });
                }
                return false;
            }
        }, '.morelink');

        return this.each(function () {
            var $this = $(this);

            var content = $this.html();
            var contentlen = $this.text().length;
            if (contentlen > config.showChars + config.minHideChars) {
                var c = content.substr(0, config.showChars);
                if (c.indexOf('<') >= 0) // If there's HTML don't want to cut it
                {
                    var inTag = false; // I'm in a tag?
                    var bag = ''; // Put the characters to be shown here
                    var countChars = 0; // Current bag size
                    var openTags = []; // Stack for opened tags, so I can close them later
                    var tagName = null;

                    for (var i = 0, r = 0; r <= config.showChars; i++) {
                        if (content[i] === '<' && !inTag) {
                            inTag = true;

                            // This could be "tag" or "/tag"
                            tagName = content.substring(i + 1, content.indexOf('>', i));

                            // If its a closing tag
                            if (tagName[0] === '/') {

                                if (tagName !== ('/' + openTags[0])) {
                                    config.errMsg = 'ERROR en HTML: the top of the stack should be the tag that closes';
                                } else {
                                    openTags.shift(); // Pops the last tag from the open tag stack (the tag is closed in the retult HTML!)
                                }

                            } else {
                                // There are some nasty tags that don't have a close tag like <br/>
                                if (tagName.toLowerCase() !== 'br') {
                                    openTags.unshift(tagName); // Add to start the name of the tag that opens
                                }
                            }
                        }

                        if (inTag && content[i] === '>') {
                            inTag = false;
                        }

                        if (inTag) {
                            bag += content.charAt(i);
                        } else {
                            // Add tag name chars to the result
                            r++;
                            if (countChars <= config.showChars) {
                                bag += content.charAt(i); // Fix to ie 7 not allowing you to reference string characters using the []
                                countChars++;
                            } else {
                                // Now I have the characters needed
                                if (openTags.length > 0) {
                                    // I have unclosed tags

                                    //console.log('They were open tags');
                                    //console.log(openTags);
                                    for (var j = 0; j < openTags.length; j++) {
                                        //console.log('Cierro tag ' + openTags[j]);
                                        bag += '</' + openTags[j] + '>'; // Close all tags that were opened

                                        // You could shift the tag from the stack to check if you end with an empty stack, that means you have closed all open tags
                                    }
                                    break;
                                }
                            }
                        }
                    }
                    c = $('<div/>').html(bag + '<span class="ellip">' + config.ellipsesText + '</span>').html();
                } else {
                    c += config.ellipsesText;
                }

                var html = '<div class="shortcontent">' + c +
                    '</div><div class="allcontent">' + content +
                    '</div><span><a href="javascript://nop/" class="morelink">' + config.moreText + '</a></span>';

                $this.html(html);
                $this.find(".allcontent").hide(); // Hide all text
                $('.shortcontent p:last', $this).css('margin-bottom', 0); //Remove bottom margin on last paragraph as it's likely shortened
            }
        });
    };
}

function finalizePage() {
    // resize all charts - without starting the background thread
    // this has to be done while NETDATA is paused
    // if we omit this, the affix menu will be wrong, since all
    // the Dom elements are initially zero-sized
    NETDATA.parseDom();

    // ------------------------------------------------------------------------

    NETDATA.globalPanAndZoom.callback = null;
    NETDATA.globalChartUnderlay.callback = null;

    if (urlOptions.pan_and_zoom === true && NETDATA.options.targets.length > 0) {
        NETDATA.globalPanAndZoom.setMaster(NETDATA.options.targets[0], urlOptions.after, urlOptions.before);
    }

    // callback for us to track PanAndZoom operations
    NETDATA.globalPanAndZoom.callback = urlOptions.netdataPanAndZoomCallback;
    NETDATA.globalChartUnderlay.callback = urlOptions.netdataHighlightCallback;

    // ------------------------------------------------------------------------

    // let it run (update the charts)
    NETDATA.unpause();

    runOnceOnDashboardWithjQuery();
    $(".shorten").shorten();
    enableTooltipsAndPopovers();

    if (isdemo()) {
        // do not to give errors on netdata demo servers for 60 seconds
        NETDATA.options.current.retries_on_data_failures = 60;

        // google analytics when this is used for the home page of the demo sites
        // this does not run on user's installations
        setTimeout(function () {
            (function (i, s, o, g, r, a, m) {
                i['GoogleAnalyticsObject'] = r;
                i[r] = i[r] || function () {
                    (i[r].q = i[r].q || []).push(arguments)
                }, i[r].l = 1 * new Date();
                a = s.createElement(o),
                    m = s.getElementsByTagName(o)[0];
                a.async = 1;
                a.src = g;
                m.parentNode.insertBefore(a, m)
            })(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');

            ga('create', 'UA-64295674-3', 'auto');
            ga('send', 'pageview', '/demosite/' + window.location.host);
        }, 2000);
    } else {
        notifyForUpdate();
    }

    if (urlOptions.show_alarms === true) {
        setTimeout(function () {
            $('#alarmsModal').modal('show');
        }, 1000);
    }

    NETDATA.onresizeCallback = function () {
        Ps.update(document.getElementById('sidebar'));
        Ps.update(document.getElementById('my-netdata-dropdown-content'));
    };
    NETDATA.onresizeCallback();

    if (netdataSnapshotData !== null) {
        NETDATA.globalPanAndZoom.setMaster(NETDATA.options.targets[0], netdataSnapshotData.after_ms, netdataSnapshotData.before_ms);
    }

    //if (urlOptions.nowelcome !== true) {
    //    setTimeout(function () {
    //        $('#welcomeModal').modal();
    //    }, 2000);
    //}

    // var netdataEnded = performance.now();
    // console.log('start up time: ' + (netdataEnded - netdataStarted).toString() + ' ms');
}

function resetDashboardOptions() {
    var help = NETDATA.options.current.show_help;

    NETDATA.resetOptions();
    if (setTheme('slate')) {
        netdataReload();
    }

    if (help !== NETDATA.options.current.show_help) {
        netdataReload();
    }
}

// callback to add the dashboard info to the
// parallel javascript downloader in netdata
var netdataPrepCallback = function () {
    NETDATA.requiredCSS.push({
        url: NETDATA.serverStatic + 'css/bootstrap-toggle-2.2.2.min.css',
        isAlreadyLoaded: function () {
            return false;
        }
    });

    NETDATA.requiredJs.push({
        url: NETDATA.serverStatic + 'lib/bootstrap-toggle-2.2.2.min.js',
        isAlreadyLoaded: function () {
            return false;
        }
    });

    NETDATA.requiredJs.push({
        url: NETDATA.serverStatic + 'dashboard_info.js?v20181019-1',
        async: false,
        isAlreadyLoaded: function () {
            return false;
        }
    });

    if (isdemo()) {
        document.getElementById('masthead').style.display = 'block';
    } else {
        if (urlOptions.update_always === true) {
            NETDATA.setOption('stop_updates_when_focus_is_lost', !urlOptions.update_always);
        }
    }
};

var selected_server_timezone = function (timezone, status) {
    //console.log('called with timezone: ' + timezone + ", status: " + ((typeof status === 'undefined')?'undefined':status).toString());

    // clear the error
    document.getElementById('timezone_error_message').innerHTML = '';

    if (typeof status === 'undefined') {
        // the user selected a timezone from the menu

        NETDATA.setOption('user_set_server_timezone', timezone);

        if (NETDATA.dateTime.init(timezone) === false) {
            NETDATA.dateTime.init();

            if (!$('#local_timezone').prop('checked')) {
                $('#local_timezone').bootstrapToggle('on');
            }

            document.getElementById('timezone_error_message').innerHTML = 'Ooops! That timezone was not accepted by your browser. Please open a github issue to help us fix it.';
            NETDATA.setOption('user_set_server_timezone', NETDATA.options.server_timezone);
        } else {
            if ($('#local_timezone').prop('checked')) {
                $('#local_timezone').bootstrapToggle('off');
            }
        }
    } else if (status === true) {
        // the user wants the browser default timezone to be activated

        NETDATA.dateTime.init();
    } else {
        // the user wants the server default timezone to be activated
        //console.log('found ' + NETDATA.options.current.user_set_server_timezone);

        if (NETDATA.options.current.user_set_server_timezone === 'default') {
            NETDATA.options.current.user_set_server_timezone = NETDATA.options.server_timezone;
        }

        timezone = NETDATA.options.current.user_set_server_timezone;

        if (NETDATA.dateTime.init(timezone) === false) {
            NETDATA.dateTime.init();

            if (!$('#local_timezone').prop('checked')) {
                $('#local_timezone').bootstrapToggle('on');
            }

            document.getElementById('timezone_error_message').innerHTML = 'Sorry. The timezone "' + timezone.toString() + '" is not accepted by your browser. Please select one from the list.';
            NETDATA.setOption('user_set_server_timezone', NETDATA.options.server_timezone);
        }
    }

    document.getElementById('current_timezone').innerText = (NETDATA.options.current.timezone === 'default') ? 'unset, using browser default' : NETDATA.options.current.timezone;
    return false;
};

// our entry point
// var netdataStarted = performance.now();

var netdataCallback = initializeDynamicDashboard;

// =================================================================================================
// netdata.cloud

let registryAgents = [];

let cloudAgents = [];

let myNetdataMenuFilterValue = "";

let cloudAccountID = null;

let cloudAccountName = null;

let cloudToken = null;

/// Enforces a maximum string length while retaining the prefix and the postfix of
/// the string.
function truncateString(str, maxLength) {
    if (str.length <= maxLength) {
        return str;
    }

    const spanLength = Math.floor((maxLength - 3) / 2);
    return `${str.substring(0, spanLength)}...${str.substring(str.length - spanLength)}`;
}

// -------------------------------------------------------------------------------------------------
// netdata.cloud API Client
// -------------------------------------------------------------------------------------------------

function isValidAgent(a) {
    return a.urls != null && a.urls.length > 0;
}

// https://github.com/netdata/hub/issues/146
function getCloudAccountAgents() {
    if (!isSignedIn()) {
        return [];
    }

    return fetch(
        `${NETDATA.registry.cloudBaseURL}/api/v1/accounts/${cloudAccountID}/agents`,
        {
            method: "GET",
            mode: "cors",
            headers: {
                "Authorization": `Bearer ${cloudToken}`
            }
        }
    ).then((response) => {
        if (!response.ok) {
            throw Error("Cannot fetch known accounts");
        }
        return response.json();
    }).then((payload) => {
        const agents = payload.result ? payload.result.agents : null;

        if (!agents) {
            return [];
        }

        return agents.filter((a) => isValidAgent(a)).map((a) => {
            return {
                "guid": a.id,
                "name": a.name,
                "url": a.urls[0],
                "alternate_urls": a.urls
            }
        })
    }).catch(function (error) {
        console.log(error);
        return null;
    });
}

/** Updates the lastAccessTime and accessCount properties of the agent for the account. */
function touchAgent() {
    if (!isSignedIn()) {
        return [];
    }

    const touchUrl = `${NETDATA.registry.cloudBaseURL}/api/v1/agents/${NETDATA.registry.machine_guid}/touch?account_id=${cloudAccountID}`;
    return fetch(
        touchUrl,
        {
            method: "post",
            body: "",
            mode: "cors",
            headers: {
                "Authorization": `Bearer ${cloudToken}`
            }
        }
    ).then((response) => {
        if (!response.ok) {
            throw Error("Cannot touch agent" + JSON.stringify(response));
        }
        return response.json();
    }).then((payload) => {

    }).catch(function (error) {
        console.log(error);
        return null;
    });
}

// https://github.com/netdata/hub/issues/128
function postCloudAccountAgents(agentsToSync) {
    if (!isSignedIn()) {
        return [];
    }

    const maskedURL = NETDATA.registry.MASKED_DATA;

    const agents = agentsToSync.map((a) => {
        const urls = a.alternate_urls.filter((url) => url != maskedURL);

        return {
            "id": a.guid,
            "name": a.name,
            "urls": urls
        }
    }).filter((a) => isValidAgent(a))

    const payload = {
        "accountID": cloudAccountID,
        "agents": agents,
        "merge": false,
    };

    return fetch(
        `${NETDATA.registry.cloudBaseURL}/api/v1/accounts/${cloudAccountID}/agents`,
        {
            method: "POST",
            mode: "cors",
            headers: {
                "Content-Type": "application/json; charset=utf-8",
                "Authorization": `Bearer ${cloudToken}`
            },
            body: JSON.stringify(payload)
        }
    ).then((response) => {
        return response.json();
    }).then((payload) => {
        const agents = payload.result ? payload.result.agents : null;

        if (!agents) {
            return [];
        }

        return agents.filter((a) => isValidAgent(a)).map((a) => {
            return {
                "guid": a.id,
                "name": a.name,
                "url": a.urls[0],
                "alternate_urls": a.urls
            }
        })
    });
}

function deleteCloudAgentURL(agentID, url) {
    if (!isSignedIn()) {
        return [];
    }

    return fetch(
        `${NETDATA.registry.cloudBaseURL}/api/v1/accounts/${cloudAccountID}/agents/${agentID}/url?value=${encodeURIComponent(url)}`,
        {
            method: "DELETE",
            mode: "cors",
            headers: {
                "Content-Type": "application/json; charset=utf-8",
                "Authorization": `Bearer ${cloudToken}`
            },
        }
    ).then((response) => {
        return response.json();
    }).then((payload) => {
        const count = payload.result ? payload.result.count : 0;
        return count;
    });
}

// -------------------------------------------------------------------------------------------------

function signInDidClick(e) {
    e.preventDefault();
    e.stopPropagation();

    if (!NETDATA.registry.isUsingGlobalRegistry()) {
        // If user is using a private registry, request his consent for
        // synchronizing with cloud.
        showSignInModal();
        return;
    }

    signIn();
}

function shouldShowSignInBanner() {
    return false;
}

function closeSignInBanner() {
    localStorage.setItem("signInBannerClosed", "true");
    const el = document.getElementById("sign-in-banner");
    if (el) {
        el.style.display = "none";
    }
}

function closeSignInBannerDidClick(e) {
    closeSignInBanner();
}

function signOutDidClick(e) {
    e.preventDefault();
    e.stopPropagation();
    signOut();
}

// -------------------------------------------------------------------------------------------------

function updateMyNetdataAfterFilterChange() {
    const machinesEl = document.getElementById("my-netdata-menu-machines")
    machinesEl.innerHTML = renderMachines(cloudAgents);

    if (options.hosts.length > 1) {
        const streamedEl = document.getElementById("my-netdata-menu-streamed")
        streamedEl.innerHTML = renderStreamedHosts(options);
    }
}

function myNetdataMenuDidShow() {
    const filterEl = document.getElementById("my-netdata-menu-filter-input");
    if (filterEl) {
        filterEl.focus();
    }
}

function myNetdataFilterDidChange(e) {
    const inputEl = e.target;
    setTimeout(() => {
        myNetdataMenuFilterValue = inputEl.value;
        updateMyNetdataAfterFilterChange();
    }, 1);
}

function myNetdataFilterClearDidClick(e) {
    e.preventDefault();
    e.stopPropagation();

    const inputEl = document.getElementById("my-netdata-menu-filter-input");
    inputEl.value = "";
    myNetdataMenuFilterValue = "";

    updateMyNetdataAfterFilterChange();

    inputEl.focus();
}

// -------------------------------------------------------------------------------------------------

function clearCloudVariables() {
    cloudAccountID = null;
    cloudAccountName = null;
    cloudToken = null;
}

function clearCloudLocalStorageItems() {
    localStorage.removeItem("cloud.baseURL");
    localStorage.removeItem("cloud.agentID");
    localStorage.removeItem("cloud.sync");
}

function signIn() {
    const url = `${NETDATA.registry.cloudBaseURL}/account/sign-in-agent?id=${NETDATA.registry.machine_guid}&name=${encodeURIComponent(NETDATA.registry.hostname)}&origin=${encodeURIComponent(window.location.origin + "/")}`;
    window.open(url);
}

function signOut() {
    cloudSSOSignOut();
}

function handleMessage(e) {
    switch (e.data.type) {
        case "sign-in":
            handleSignInMessage(e);
            break;

        case "sign-out":
            handleSignOutMessage(e);
            break;

        default:
            return;
    }
}

function handleSignInMessage(e) {
    closeSignInBanner();
    localStorage.setItem("cloud.baseURL", NETDATA.registry.cloudBaseURL);

    cloudAccountID = e.data.accountID;
    cloudAccountName = e.data.accountName;
    cloudToken = e.data.token;

    netdataRegistryCallback(registryAgents);
    if (e.data.redirectURI && !window.location.href.includes(e.data.redirectURI)) {
        window.location.replace(e.data.redirectURI);
    }
}

function handleSignOutMessage(e) {
    clearCloudVariables();
    renderMyNetdataMenu(registryAgents);
}

function isSignedIn() {
    return cloudToken != null && cloudAccountID != null;
}

function sortedArraysEqual(a, b) {
    if (a.length != b.length) return false;

    for (var i = 0; i < a.length; ++i) {
        if (a[i] !== b[i]) return false;
    }

    return true;
}

// If merging is needed returns the merged agents set, otherwise returns null.
function mergeAgents(cloud, local) {
    let dirty = false;

    const union = new Map();

    for (const cagent of cloud) {
        union.set(cagent.guid, cagent);
    }

    for (const lagent of local) {
        const cagent = union.get(lagent.guid);
        if (cagent) {
            for (const u of lagent.alternate_urls) {
                if (u === NETDATA.registry.MASKED_DATA) { // TODO: temp until registry is updated.
                    continue;
                }

                if (!cagent.alternate_urls.includes(u)) {
                    dirty = true;
                    cagent.alternate_urls.push(u);
                }
            }
        } else {
            dirty = true;
            union.set(lagent.guid, lagent);
        }
    }

    if (dirty) {
        return Array.from(union.values());
    }

    return null;
}

function showSignInModal() {
    document.getElementById("sim-registry").innerHTML = NETDATA.registry.server;
    $("#signInModal").modal("show");
}

function explicitlySignIn() {
    $("#signInModal").modal("hide");
    signIn();
}

function showSyncModal() {
    document.getElementById("sync-registry-modal-registry").innerHTML = NETDATA.registry.server;
    $("#syncRegistryModal").modal("show");
}

function explicitlySyncAgents() {
    $("#syncRegistryModal").modal("hide");

    const json = localStorage.getItem("cloud.sync");
    const sync = json ? JSON.parse(json) : {};
    delete sync[cloudAccountID];
    localStorage.setItem("cloud.sync", JSON.stringify(sync));

    NETDATA.registry.init();
}

function syncAgents(callback) {
    const json = localStorage.getItem("cloud.sync");
    const sync = json ? JSON.parse(json) : {};

    const currentAgent = {
        guid: NETDATA.registry.machine_guid,
        name: NETDATA.registry.hostname,
        url: NETDATA.serverDefault,
        alternate_urls: [NETDATA.serverDefault],
    }

    const localAgents = sync[cloudAccountID]
        ? [currentAgent]
        : registryAgents.concat([currentAgent]);

    console.log("Checking if sync is needed.", localAgents);

    const agentsToSync = mergeAgents(cloudAgents, localAgents);

    if ((!sync[cloudAccountID]) || agentsToSync) {
        sync[cloudAccountID] = new Date().getTime();
        localStorage.setItem("cloud.sync", JSON.stringify(sync));
    }

    if (agentsToSync) {
        console.log("Synchronizing with netdata.cloud.");

        postCloudAccountAgents(agentsToSync).then((agents) => {
            // TODO: clear syncTime on error!
            cloudAgents = agents;
            callback(cloudAgents);
        });

        return
    }

    callback(cloudAgents);
}

let isCloudSSOInitialized = false;

function cloudSSOInit() {
    const iframeEl = document.getElementById("ssoifrm");
    const url = `${NETDATA.registry.cloudBaseURL}/account/sso-agent?id=${NETDATA.registry.machine_guid}`;
    iframeEl.src = url;
    isCloudSSOInitialized = true;
}

function cloudSSOSignOut() {
    const iframe = document.getElementById("ssoifrm");
    const url = `${NETDATA.registry.cloudBaseURL}/account/sign-out-agent`;
    iframe.src = url;
}

function initCloud() {
    if (!NETDATA.registry.isCloudEnabled) {
        clearCloudVariables();
        clearCloudLocalStorageItems();
        return;
    }

    if (NETDATA.registry.cloudBaseURL != localStorage.getItem("cloud.baseURL")) {
        clearCloudVariables();
        clearCloudLocalStorageItems();
        if (NETDATA.registry.cloudBaseURL) {
            localStorage.setItem("cloud.baseURL", NETDATA.registry.cloudBaseURL);
        }
    }

    if (!isCloudSSOInitialized) {
        cloudSSOInit();
    }

    touchAgent();
}

// This callback is called after NETDATA.registry is initialized.
function netdataRegistryCallback(machinesArray) {
    localStorage.setItem("cloud.agentID", NETDATA.registry.machine_guid);

    initCloud();

    registryAgents = machinesArray;

    if (isSignedIn()) {
        // We call getCloudAccountAgents() here because it requires that
        // NETDATA.registry is initialized.
        clearMyNetdataMenu();
        getCloudAccountAgents().then((agents) => {
            if (!agents) {
                errorMyNetdataMenu();
                return;
            }
            cloudAgents = agents;
            syncAgents((agents) => {
                const agentsMap = {}
                for (const agent of agents) {
                    agentsMap[agent.guid] = agent;
                }

                NETDATA.registry.machines = agentsMap;
                NETDATA.registry.machines_array = agents;

                renderMyNetdataMenu(agents);
            });
        });
    } else {
        renderMyNetdataMenu(machinesArray)
    }
};

// If we know the cloudBaseURL and agentID from local storage render (eagerly)
// the account ui before receiving the definitive response from the web server.
// This improves the perceived performance.
function tryFastInitCloud() {
    const baseURL = localStorage.getItem("cloud.baseURL");
    const agentID = localStorage.getItem("cloud.agentID");

    if (baseURL && agentID) {
        NETDATA.registry.cloudBaseURL = baseURL;
        NETDATA.registry.machine_guid = agentID;
        NETDATA.registry.isCloudEnabled = true;

        initCloud();
    }
}

function initializeApp() {
    window.addEventListener("message", handleMessage, false);

    //    tryFastInitCloud();
}

if (document.readyState === "complete") {
    initializeApp();
} else {
    document.addEventListener("readystatechange", () => {
        if (document.readyState === "complete") {
            initializeApp();
        }
    });
}