Blob Blame History Raw
--[[
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements.  See the NOTICE file
distributed with this work for additional information
regarding copyright ownership.  The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License.  You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied.  See the License for the
specific language governing permissions and limitations
under the License.
]]

--[[ mod_lua implementation of the server-status page ]]
local ssversion = "0.11" -- verion of this script
local redact_ips = true -- whether to replace the last two bits of every IP with 'x.x'
local warning_banner = [[
    <div style="float: left; color: #222; margin-bottom: 8px; margin-top: 24px; text-align: center; width: 200px; font-size: 0.7rem; border: 1px dashed #333; background: #F8C940;">
        <h3 style="margin: 4px; font-size: 1rem;">Don't be alarmed - this page is here for a reason!</h3>
        <p style="font-weight: bolder; font-size: 0.8rem;">This is an example server status page for the Apache HTTP Server. Nothing on this server is secret, no URL tokens, no sensitive passwords. Everything served from here is static data.</p>
    </div>
]]
local show_warning = true -- whether to display the above warning/notice on the page
local show_modules = false -- Whether to list loaded modules or not
local show_threads = true -- whether to list thread information or not

-- pre-declare some variables defined at the bottom of this script:
local status_js, status_css, quokka_js

-- quick and dirty JSON conversion
local function quickJSON(input)
    if type(input) == "table" then
        local t = 'array'
        for k, v in pairs(input) do
            if type(k) ~= "number" then
                t = 'hash'
                break
            end
        end
        
        if t == 'hash' then
            local out = ""
            local tbl = {}
            for k, v in pairs(input) do
                local kv = ([["%s": %s]]):format(k, quickJSON(v))
                table.insert(tbl, kv)
            end
            return "{" .. table.concat(tbl, ", ") .. "}"
        else
            local tbl = {}
            for k, v in pairs(input) do
                table.insert(tbl, quickJSON(v))
            end
            return "[" .. table.concat(tbl, ", ") .. "]"
        end
    elseif type(input) == "string" then
        return ([["%s"]]):format(input:gsub('"', '\\"'):gsub("[\r\n\t]", " "))
    elseif type(input) == "number" then
        return tostring(input)
    elseif type(input) == "boolean" then
        return (input and "true" or "false")
    else
        return "null"
    end
end

-- Module information callback
local function modInfo(r, modname)
    if modname then
            r:puts [[
    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8">
        <style>
        ]]
        r:puts (status_css)
        r:puts [[
        </style>
        <title>Module information</title>
      </head>
    
      <body>
    ]]
        r:puts( ("<h3>Details for module %s</h3>\n"):format(r:escape_html(modname)) )
        -- Queries the server for information about a module
        local mod = r.module_info(modname)
        if mod then
            for k, v in pairs(mod.commands) do
                -- print out all directives accepted by this module
                r:puts( ("<b>%s:</b> %s<br>\n"):format(r:escape_html(k), v))
            end
        end
        -- HTML tail
        r:puts[[
      </body>
    </html>
    ]]
    end
end

-- Function for generating server stats
function getServerState(r, verbose)
    local state = {}
    
    state.mpm = {
        type = "prefork", -- default to prefork until told otherwise
        threadsPerChild = 1,
        threaded = false,
        maxServers = r.mpm_query(12),
        activeServers = 0
    }
    if r.mpm_query(14) == 1 then
        state.mpm.type = "event" -- this is event mpm
    elseif r.mpm_query(3) >= 1 then
        state.mpm.type = "worker" -- it's not event, but it's threaded, we'll assume worker mpm (could be motorz??)
    elseif r.mpm_query(2) == 1 then
        state.mpm.type = "winnt" -- it's threaded, but not worker nor event, so it's probably winnt
    end
    if state.mpm.type ~= "prefork" then
        state.mpm.threaded = true -- it's threaded
        state.mpm.threadsPerChild = r.mpm_query(6) -- get threads per child proc
    end
    
    state.processes = {} -- list of child procs
    state.connections = { -- overall connection info
        idle = 0,
        active = 0
    }
    -- overall server stats
    state.server = {
        connections = 0,
        bytes = 0,
        built = r.server_built,
        localtime = os.time(),
        uptime = os.time() - r.started,
        version = r.banner,
        host = r.server_name,
        modules = nil,
        extended = show_threads, -- whether extended status is available or not
    }
    
    -- if show_modules is true, add list of modules to the JSON
    if show_modules then
        state.server.modules = {}
        for k, module in pairs(r:loaded_modules()) do
            table.insert(state.server.modules, module)
        end
    end
    
    -- Fetch process/thread data
    for i=0,state.mpm.maxServers-1,1 do
        local server = r.scoreboard_process(r, i);
        if server then
            local s = {
                active = false,
                pid = nil,
                bytes = 0,
                stime = 0,
                utime = 0,
                connections = 0,
            }
            local tstates = {}
            if server.pid then
                state.connections.idle = state.connections.idle + (server.keepalive or 0)
                s.connections = 0
                if server.pid > 0 then
                    state.mpm.activeServers = state.mpm.activeServers + 1
                    s.active = true
                    s.pid = server.pid
                end
                for j = 0, state.mpm.threadsPerChild-1, 1 do
                    local worker = r.scoreboard_worker(r, i, j)
                    if worker then
                        s.stime = s.stime + (worker.stimes or 0);
                        s.utime = s.utime + (worker.utimes or 0);
                        if verbose and show_threads then
                            s.threads = s.threads or {}
                            table.insert(s.threads, {
                                bytes = worker.bytes_served,
                                thread = ("0x%x"):format(worker.tid),
                                client = redact_ips and (worker.client or "???"):gsub("[a-f0-9]+[.:]+[a-f0-9]+$", "x.x") or worker.client or "???",
                                cost = ((worker.utimes or 0) + (worker.stimes or 0)),
                                count = worker.access_count,
                                vhost = worker.vhost:gsub(":%d+", ""),
                                request = worker.request,
                                last_used = math.floor(worker.last_used/1000000)
                            })
                        end
                        state.server.connections = state.server.connections + worker.access_count
                        s.bytes = s.bytes + worker.bytes_served
                        s.connections = s.connections + worker.access_count
                        if server.pid > 0 then
                            tstates[worker.status] = (tstates[worker.status] or 0) + 1
                        end
                    end
                end
            end
            
            s.workerStates = {
                keepalive = (server.keepalive > 0) and server.keepalive or tstates[5] or 0,
                closing = tstates[8] or 0,
                idle = tstates[2] or 0,
                writing = tstates[4] or 0,
                reading = tstates[3] or 0,
                graceful = tstates[9] or 0
            }
            table.insert(state.processes, s)
            state.server.bytes = state.server.bytes + s.bytes
            state.connections.active = state.connections.active + (tstates[8] or 0) + (tstates[4] or 0) + (tstates[3] or 0)
        end
    end
    return state
end

-- Handler function
function handle(r)
    
    -- Parse GET data, if any, and set content type
    local GET = r:parseargs()
    
    if GET['module'] then
        modInfo(r, GET['module'])
        return apache2.OK
    end


    -- If we only need the stats feed, compact it and hand it over
    if GET['view'] and GET['view'] == "json" then
        local state = getServerState(r, GET['extended'] == 'true')
        r.content_type = "application/json"
        r:puts(quickJSON(state))
        return apache2.OK
    end
    
    if not GET['resource'] then
    
        local state = getServerState(r, show_threads)
        
        -- Print out the HTML for the front page
        r.content_type = "text/html"
        r:puts ( ([=[
    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8">
        <!-- Stylesheet -->
        <link href="?resource=css" rel="stylesheet">
        
        <!-- JavaScript-->
        <script type="text/javascript" src="?resource=js"></script>
        
        <title>Server status for %s</title>
      </head>
    
      <body onload="refreshCharts(false);">
        <div class="wrapper" id="wrapper">
            <div class="navbarLeft">
                <img align='absmiddle' src='?resource=feather' width="15" height="30"/>
                Apache HTTPd
            </div>
            <div class="navbarRight">Status for %s on %s</div>
            <div style="clear: both;"></div>
            <div class="serverinfo" id="leftpane">
                <ul id="menubar">
                    <li>
                        <a class="btn active" id="dashboard_button" href="javascript:void(showPanel('dashboard'));">Dashboard</a>
                    </li>
                    <li>
                        <a class="btn" id="misc_button" href="javascript:void(showPanel('misc'));">Server Info</a>
                    </li>
                    <li>
                        <a class="btn" id="threads_button" style="display: none;" href="javascript:void(showPanel('threads'));">Show thread information</a>
                    </li>
                    <li>
                        <a class="btn" id="modules_button" style="display: none;" href="javascript:void(showPanel('modules'));">Show loaded modules</a>
                    </li>
                </ul>
                
                <!-- warning --> %s <!-- /warning -->
                
            </div>
            
            <!-- dashboard -->
            <div class="charts" id="dashboard_panel">
            
                <div class="infobox_wrapper" style="clear: both; width: 100%%;">
                    <div class="infobox_title">Quick Stats</div>
                    <div class="infobox" id="general_stats">
                    </div>
                </div>
                <div class="infobox_wrapper" style="width: 100%%;">
                    <div class="infobox_title">Charts</div>
                    <div class="infobox">
                        <!--Div that will hold the pie chart-->
                        <canvas id="actions_div" width="1400" height="400" class="canvas_wide"></canvas>
                        <canvas id="status_div" width=580" height="400" class="canvas_narrow"></canvas>
                        <canvas id="traffic_div" width="1400" height="400" class="canvas_wide"></canvas>
                        <canvas id="idle_div" width="580" height="400" class="canvas_narrow"></canvas>
                        <canvas id="connection_div" width="1400" height="400" class="canvas_wide"></canvas>
                        <canvas id="cpu_div" width="580" height="400" class="canvas_narrow"></canvas>
                        <div style="clear: both"></div>
                    </div>
                </div>
            </div>
            
            <!-- misc server info -->
            <div class="charts" id="misc_panel" style="display: none;">
                <div class="infobox_wrapper" style="clear: both; width: 100%%;">
                    <div class="infobox_title">General server information</div>
                    <div class="infobox" style='padding: 16px; width: calc(100%% - 32px);' id="server_breakdown">
                    </div>
                </div>
            </div>
            
            <!-- thread info -->
            <div class="charts" id="threads_panel" style="display: none;">
                <div class="infobox_wrapper" style="clear: both; width: 100%%;">
                    <div class="infobox_title">Thread breakdown</div>
                    <div class="infobox" style='padding: 16px; width: calc(100%% - 32px);' id="threads_breakdown">
                    </div>
                </div>
            </div>
            
            <!-- module info -->
            <div class="charts" id="modules_panel" style="display: none;">
                <div class="infobox_wrapper" style="clear: both; width: 100%%;">
                    <div class="infobox_title">Modules loaded</div>
                    <div class="infobox" style='padding: 16px; width: calc(100%% - 32px);' id="modules_breakdown">
                    blabla
                    </div>
                </div>
            </div>
            
            
        </div>
    
    
    ]=]):format(
        r.server_name,
        r.banner,
        r.server_name,
        show_warning and warning_banner or ""
        ) );
        -- HTML tail
        r:puts[[
        </body>
      </html>
      ]]
    else
        -- Resource documents (CSS, JS, PNG)
        if GET['resource'] == 'js' then
            r.content_type = "application/javascript"
            r:puts(quokka_js)
            r:puts(status_js)
        elseif GET['resource'] == 'css' then
            r.content_type = "text/css"
            r:puts(status_css)
        elseif GET['resource'] == 'feather' then
            r.content_type = "image/png"
            r:write(r:base64_decode('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'))
        end
    end
    return apache2.OK;
end


------------------------------------
-- JavaScript and CSS definitions --
------------------------------------

-- Set up some JavaScripts:
status_js = [==[
Number.prototype.pad = function(size) {
    var str = String(this);
    while (str.length < size) {
        str = "0" + str;
    }
    return str;
}

function getAsync(theUrl, xstate, callback) {
    var xmlHttp = null;
    if (window.XMLHttpRequest) {
	xmlHttp = new XMLHttpRequest();
    } else {
	xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
    }
    xmlHttp.open("GET", theUrl, true);
    xmlHttp.send(null);
    xmlHttp.onreadystatechange = function(state) {
        if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {
            if (callback) {
                callback(JSON.parse(xmlHttp.responseText));
            }
            
        }
    }
}

var actionCache = [];
var connectionCache = [];
var trafficCache = [];
var processes = {};
var lastBytes = 0;
var lastConnections = 0;
var negativeBytes = 0; // cache for proc reloads, which skews traffic
var updateSpeed = 5; // How fast do charts update?
var maxRecords = 24; // How many records to show per chart
var cpumax = 1000000; // random cpu max(?)

function refreshCharts(json, state) {
    if (json && json.processes) {
        
        
        
         // general server info box
        var gs = document.getElementById('server_breakdown');
        gs.innerHTML = "";
        gs.innerHTML += "<b>Server version: </b>" + json.server.version + "<br/>";
        gs.innerHTML += "<b>Server built: </b>" + json.server.built + "<br/>";
        gs.innerHTML += "<b>Server MPM: </b>" + json.mpm.type + " <span id='mpminfo'></span><br/>";
        
        
        // Get a timestamp
        var now = new Date();
        var ts = now.getHours().pad(2) + ":" + now.getMinutes().pad(2) + ":" + now.getSeconds().pad(2);
        
        var utime = 0;
        var stime = 0;
        
        // Construct state based on proc details
        var state = {
            timestamp: ts,
            closing: 0,
            idle: 0,
            writing: 0,
            reading: 0,
            keepalive: 0,
            graceful: 0
        }
        for (var i in json.processes) {
            var proc = json.processes[i];
            if (proc.pid) {
                state.closing += proc.workerStates.closing||0;
                state.idle += proc.workerStates.idle||0;
                state.writing += proc.workerStates.writing||0;
                state.reading += proc.workerStates.reading||0;
                state.keepalive += proc.workerStates.keepalive||0;
                state.graceful += proc.workerStates.graceful||0;
                utime += proc.utime;
                stime += proc.stime;
            }
        }
        
        // Push action state entry into action cache with timestamp
        // Shift if more than 10 entries in cache
        actionCache.push(state);
        if (actionCache.length > maxRecords) {
            actionCache.shift();
        }
        
        // construct array for QuokkaLines
        var arr = [];
        for (var i in actionCache) {
            var el = actionCache[i];
            if (json.mpm.type == 'event') {
            arr.push([el.timestamp, el.closing, el.idle, el.writing, el.reading, el.graceful]);
            } else {
                arr.push([el.timestamp, el.keepalive, el.closing, el.idle, el.writing, el.reading, el.graceful]);
            }
        }
        var states = ['Keepalive', 'Closing', 'Idle', 'Writing', 'Reading', 'Graceful']
        if (json.mpm.type == 'event') {
            states.shift();
            if (document.getElementById('mpminfo')) {
                document.getElementById('mpminfo').innerHTML = "(" + fn(parseInt(json.connections.idle)) + " connections in idle keepalive)";
            }
        }
        // Draw action chart
        quokkaLines("actions_div", states, arr, { lastsum: true, hires: true, nosum: true, stack: true, curve: true, title: "Thread states" } );
        
        
        // Get traffic, figure out how much it was this time (0 if just started!)
        var bytesThisTurn = 0;
        var connectionsThisTurn = 0;
        for (var i in json.processes) {
            var proc = json.processes[i];
            var pid = proc.pid
            // if we haven't seen this proc before, ignore its bytes first time
            if (!processes[pid]) {
                processes[pid] = {
                    bytes: proc.bytes,
                    connections: proc.connections,
                }
            } else {
                bytesThisTurn += proc.bytes - processes[pid].bytes;
                if (pid) {
                    x = proc.connections - processes[pid].connections;
                    connectionsThisTurn += (x > 0) ? x : 0;
                }
                processes[pid].bytes = proc.bytes;
                processes[pid].connections = proc.connections;
            }
        }
        
        if (lastBytes == 0 ) {
            bytesThisTurn = 0;
        }
        lastBytes = 1;

        // Push a new element into cache, prune cache
        var el = {
            timestamp: ts,
            bytes: bytesThisTurn/updateSpeed
        };
        trafficCache.push(el);
        if (trafficCache.length > maxRecords) {
            trafficCache.shift();
        }
        
        // construct array for QuokkaLines
        arr = [];
        for (var i in trafficCache) {
            var el = trafficCache[i];
            arr.push([el.timestamp, el.bytes]);
        }
        // Draw action chart
        quokkaLines("traffic_div", ['Traffic'], arr, { traffic: true, hires: true, nosum: true, stack: true, curve: true, title: "Traffic per second" } );
        
        
        // Get connections per second
        // Push a new element into cache, prune cache
        var el = {
            timestamp: ts,
            connections: (connectionsThisTurn+1)/updateSpeed
        };
        connectionCache.push(el);
        if (connectionCache.length > maxRecords) {
            connectionCache.shift();
        }
        
        // construct array for QuokkaLines
        arr = [];
        for (var i in connectionCache) {
            var el = connectionCache[i];
            arr.push([el.timestamp, el.connections]);
        }
        // Draw connection chart
        quokkaLines("connection_div", ['Connections/sec'], arr, { traffic: false, hires: true, nosum: true, stack: true, curve: true, title: "Connections per second" } );
        
        
        // Thread info
        quokkaCircle("status_div", [
        { title: 'Active', value: (json.mpm.threadsPerChild*json.mpm.activeServers)},
        { title: 'Reserve', value: (json.mpm.threadsPerChild*(json.mpm.activeServers-json.mpm.maxServers))}
        ],
            { title: "Worker pool", hires: true});
        
        // Idle vs active connections
        var idlecons = json.connections.idle;
        var activecons = json.connections.active;
        quokkaCircle("idle_div", [
            { title: 'Idle', value: idlecons},
            { title: 'Active', value: activecons},
            ],
            { hires: true, title: "Idle vs active connections"});
        
        
        // CPU info
        while ( (stime+utime) > cpumax ) {
            cpumax = cpumax * 2;
        }

        quokkaCircle("cpu_div", [
            { title: 'Idle', value: (cpumax - stime - utime) / (cpumax/100)},
            { title: 'System', value: stime/(cpumax/100)},
            { title: 'User', value: utime/(cpumax/100)}
            ],
            { hires: true, title: "CPU usage", pct: true});
        
        
        
        
        
        
        // General stats infobox
        var gstats = document.getElementById('general_stats');
        gstats.innerHTML = ''; // wipe the box
        
            // Days since restart
            var u_f = Math.floor(json.server.uptime/8640.0) / 10;
            var u_d = Math.floor(json.server.uptime/86400);
            var u_h = Math.floor((json.server.uptime%86400)/3600);
            var u_m = Math.floor((json.server.uptime%3600)/60);
            var u_s = Math.floor(json.server.uptime %60);
            var str =  u_d + " day" + (u_d != 1 ? "s, " : ", ") + u_h + " hour" + (u_h != 1 ? "s, " : ", ") + u_m + " minute" + (u_m != 1 ? "s" : "");
            var ubox = document.createElement('div');
            ubox.setAttribute("class", "statsbox");
            ubox.innerHTML = "<span style='font-size: 2rem;'>" + u_f + " days</span><br/><i>since last (re)start.</i><br/><small>" + str;
            gstats.appendChild(ubox);
            
            
            // Bytes transferred in total
            var MB = fnmb(json.server.bytes);
            var KB = (json.server.bytes > 0) ? fnmb(json.server.bytes/json.server.connections) : 0;
            var KBs = fnmb(json.server.bytes/json.server.uptime);
            var mbbox = document.createElement('div');
            mbbox.setAttribute("class", "statsbox");
            mbbox.innerHTML = "<span style='font-size: 2rem;'>" + MB + "</span><br/><i>transferred in total.</i><br/><small>" + KBs + "/sec, " + KB + "/request";
            gstats.appendChild(mbbox);
            
            // connections in total
            var cons = fn(json.server.connections);
            var cps = Math.floor(json.server.connections/json.server.uptime*100)/100;
            var conbox = document.createElement('div');
            conbox.setAttribute("class", "statsbox");
            conbox.innerHTML = "<span style='font-size: 2rem;'>" + cons + " conns</span><br/><i>since server started.</i><br/><small>" + cps + " requests per second";
            gstats.appendChild(conbox);
            
            // threads working
            var tpc = json.mpm.threadsPerChild;
            var activeThreads = fn(json.mpm.activeServers * json.mpm.threadsPerChild);
            var maxThreads = json.mpm.maxServers * json.mpm.threadsPerChild;
            var tbox = document.createElement('div');
            tbox.setAttribute("class", "statsbox");
            tbox.innerHTML = "<span style='font-size: 2rem;'>" + activeThreads + " threads</span><br/><i>currently at work (" + json.mpm.activeServers + "x" + tpc+" threads).</i><br/><small>" + maxThreads + " (" + json.mpm.maxServers + "x"+tpc+") threads allowed.";
            gstats.appendChild(tbox);
        
        
        
        window.setTimeout(waitTwo, updateSpeed*1000);
        
        // resize pane
        document.getElementById('leftpane').style.height = document.getElementById('wrapper').getBoundingClientRect().height + "px";
        
        // Do we have extended info and module lists??
        if (json.server.extended) document.getElementById('threads_button').style.display = 'block';
        if (json.server.modules && json.server.modules.length > 0) {
            var panel = document.getElementById('modules_breakdown');
            var list = "<ul>";
            for (var i in json.server.modules) {
                var mod = json.server.modules[i];
                list += "<li>" + mod + "</li>";
            }
            list += "</ul>";
            panel.innerHTML = list;
            
            document.getElementById('modules_button').style.display = 'block';
        }
       
        
    } else if (json === false) {
        waitTwo();
    }
}

function refreshThreads(json, state) {
    var box = document.getElementById('threads_breakdown');
    box.innerHTML = "";
    for (var i in json.processes) {
        var proc = json.processes[i];
        var phtml = '<div style="color: #DDF">';
        if (!proc.active) phtml = '<div title="this process is inactive" style="color: #999;">';
        phtml += "<h3>Process " + i + ":</h3>";
        phtml += "<b>PID:</b> " + (proc.pid||"None (not active)") + "<br/>";
        if (proc.threads && proc.active) {
            phtml += "<table style='width: 800px; color: #000;'><tr><th>Thread ID</th><th>Access count</th><th>Bytes served</th><th>Last Used</th><th>Last client</th><th>Last request</th></tr>";
            for (var j in proc.threads) {
                var thread = proc.threads[j];
                thread.request = (thread.request||"(Unknown)").replace(/[<>]+/g, "");
                phtml += "<tr><td>"+thread.thread+"</td><td>"+thread.count+"</td><td>"+thread.bytes+"</td><td>"+thread.last_used+"</td><td>"+thread.client+"</td><td>"+thread.request+"</td></tr>";
            }
            phtml += "</table>";
        } else {
            phtml += "<p>No thread information avaialable</p>";
        }
        phtml += "</div>";
        box.innerHTML += phtml;
    }
}

function waitTwo() {
    getAsync(location.href + "?view=json&rnd=" + Math.random(), null, refreshCharts)
}

    function showPanel(what) {
        var items = ['dashboard','misc','threads','modules'];
        for (var i in items) {
            var item = items[i];
            var btn = document.getElementById(item+'_button');
            var panel = document.getElementById(item+'_panel');
            if (item == what) {
                btn.setAttribute("class", "btn active");
                panel.style.display = 'block';
            } else {
                btn.setAttribute("class", "btn");
                panel.style.display = 'none';
            }
        }
        
        // special constructors
        if (what == 'threads') {
            getAsync(location.href + "?view=json&extended=true&rnd=" + Math.random(), null, refreshThreads)
        }
    }
    
    function fn(num) {
        num = num + "";
        num = num.replace(/(\d)(\d{9})$/, '$1,$2');
        num = num.replace(/(\d)(\d{6})$/, '$1,$2');
        num = num.replace(/(\d)(\d{3})$/, '$1,$2');
        return num;
    }

    function fnmb(num) {
        var add = "bytes";
        var dec = "";
        var mul = 1;
        if (num > 1024) { add = "KB"; mul= 1024; }
        if (num > (1024*1024)) { add = "MB"; mul= 1024*1024; }
        if (num > (1024*1024*1024)) { add = "GB"; mul= 1024*1024*1024; }
        if (num > (1024*1024*1024*1024)) { add = "TB"; mul= 1024*1024*1024*1024; }
        num = num / mul;
        if (add != "bytes") {
            dec = "." + Math.floor( (num - Math.floor(num)) * 100 );
        }
        return ( fn(Math.floor(num)) + dec + " " + add );
    }

    function sort(a,b){
        last_col = -1;
        var sort_reverse = false;
        var sortWay = a.getAttribute("sort_" + b);
        if (sortWay && sortWay == "forward") {
            a.setAttribute("sort_" + b, "reverse");
            sort_reverse = true;
        }
        else {
            a.setAttribute("sort_" + b, "forward");
        }
        var c,d,e,f,g,h,i;
        c=a.rows.length;
        if(c<1){ return; }
        d=a.rows[1].cells.length;
        e=1;
        var j=new Array(c);
        f=0;
        for(h=e;h<c;h++){
            var k=new Array(d);
            for(i=0;i<d;i++){
                cell_text="";
                cell_text=a.rows[h].cells[i].textContent;
                if(cell_text===undefined){cell_text=a.rows[h].cells[i].innerText;}
                k[i]=cell_text;
            }
            j[f++]=k;
        }
        var l=false;
        var m,n;
        if(b!=lastcol) lastseq="A";
        else{
            if(lastseq=="A") lastseq="D";
            lastseq="A";
        }

        g=c-1;

        for(h=0;h<g;h++){
            l=false;
            for(i=0;i<g-1;i++){
                m=j[i];
                n=j[i+1];
                if(lastseq=="A"){
                    var gt = (m[b]>n[b]) ? true : false;
                    var lt = (m[b]<n[b]) ? true : false;
                    if (n[b].match(/^(\d+)$/)) { gt = parseInt(m[b], 10) > parseInt(n[b], 10) ? true : false; lt = parseInt(m[b], 10) < parseInt(n[b], 10) ? true : false; }
                    if (sort_reverse) {gt = (!gt); lt = (!lt);}
                    if(gt){
                        j[i+1]=m;
                        j[i]=n;
                        l=true;
                    }
                }
                else{
                    if(lt){
                        j[i+1]=m;
                        j[i]=n;
                        l=true;
                    }
                }
            }
            if(l===false){
                break;
            }
        }
        f=e;
        for(h=0;h<g;h++){
            m=j[h];
            for(i=0;i<d;i++){
                if(a.rows[f].cells[i].innerText!==undefined){
                    a.rows[f].cells[i].innerText=m[i];
                }
                else{
                    a.rows[f].cells[i].textContent=m[i];
                }
            }
            f++;
        }
        lastcol=b;
    }

    
    var CPUmax =            1000000;
    
    
    var showing = false;
    function showDetails() {
        for (i=1; i < 1000; i++) {
            var obj = document.getElementById("srv_" + i);
            if (obj) {
                if (showing) { obj.style.display = "none"; }
                else { obj.style.display = "block"; }
            }
        }
        var link = document.getElementById("show_link");
        showing = (!showing);
        if (showing) { link.innerHTML = "Hide thread information"; }
        else { link.innerHTML = "Show thread information"; }
    }

    var showing_modules = false;
    function show_modules() {

        var obj = document.getElementById("modules");
        if (obj) {
            if (showing_modules) { obj.style.display = "none"; }
            else { obj.style.display = "block"; }
        }
        var link = document.getElementById("show_modules_link");
        showing_modules = (!showing_modules);
        if (showing_modules) { link.innerHTML = "Hide loaded modules"; }
        else { link.innerHTML = "Show loaded modules"; }
    }
]==]

quokka_js = [==[
/*
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// Traffic shaper
function quokka_fnmb(num) {
    var add = "b";
    var dec = "";
    var mul = 1;
    if (num > 1024) { add = "KB"; mul= 1024; }
    if (num > (1024*1024)) { add = "MB"; mul= 1024*1024; }
    if (num > (1024*1024*1024)) { add = "GB"; mul= 1024*1024*1024; }
    if (num > (1024*1024*1024*1024)) { add = "TB"; mul= 1024*1024*1024*1024; }
    num = num / mul;
    if (add != "b" && num < 10) {
        dec = "." + Math.floor( (num - Math.floor(num)) * 100 );
    }
    return ( Math.floor(num) + dec + " " + add );
}

// Hue, Saturation and Lightness to Red, Green and Blue:
function quokka_internal_hsl2rgb (h,s,l)
{
    var min, sv, switcher, fract, vsf;
    h = h % 1;
    if (s > 1) s = 1;
    if (l > 1) l = 1;
    var v = (l <= 0.5) ? (l * (1 + s)) : (l + s - l * s);
    if (v === 0)
        return { r: 0, g: 0, b: 0 };

    min = 2 * l - v;
    sv = (v - min) / v;
    var sh = (6 * h) % 6;
    switcher = Math.floor(sh);
    fract = sh - switcher;
    vsf = v * sv * fract;

    switch (switcher)
    {
        case 0: return { r: v, g: min + vsf, b: min };
        case 1: return { r: v - vsf, g: v, b: min };
        case 2: return { r: min, g: v, b: min + vsf };
        case 3: return { r: min, g: v - vsf, b: v };
        case 4: return { r: min + vsf, g: min, b: v };
        case 5: return { r: v, g: min, b: v - vsf };
    }
    return {r:0, g:0, b: 0};
}

// RGB to Hex conversion
function quokka_internal_rgb2hex(r, g, b) {
    return "#" + ((1 << 24) + (Math.floor(r) << 16) + (Math.floor(g) << 8) + Math.floor(b)).toString(16).slice(1);
}


// Generate color list used for charts
var colors = [];
var rgbs = []
var numColorRows = 6;
var numColorColumns = 20;
for (var x=0;x<numColorRows;x++) {
    for (var y=0;y<numColorColumns;y++) {
        var rnd = [[148, 221, 119], [0, 203, 171], [51, 167, 215] , [35, 160, 253], [218, 54, 188], [16, 171, 246], [110, 68, 206], [21, 49, 248], [142, 104, 210]][y]
        var color = quokka_internal_hsl2rgb(y > 8 ? (Math.random()) : (rnd[0]/255), y > 8 ? (0.75+(y*0.05)) : (rnd[1]/255), y > 8 ? (0.42 + (y*0.05*(x/numColorRows))) : (0.1 + rnd[2]/512));
        
        // Light (primary) color:
        var hex = quokka_internal_rgb2hex(color.r*255, color.g*255, color.b*255);
        
        // Darker variant for gradients:
        var dhex = quokka_internal_rgb2hex(color.r*131, color.g*131, color.b*131);
        
        // Medium variant for legends:
        var mhex = quokka_internal_rgb2hex(color.r*200, color.g*200, color.b*200);
        
        colors.push([hex, dhex, color, mhex]);
    }
}


/* Function for drawing pie diagrams
 * Example usage:
 * quokkaCircle("canvasName", [ { title: 'ups', value: 30}, { title: 'downs', value: 70} ] );
 */

function quokkaCircle(id, tags, opts) {
    // Get Canvas object and context
    var canvas = document.getElementById(id);
    var ctx=canvas.getContext("2d");
    
    // Calculate the total value of the pie
    var total = 0;
    var k;
    for (k in tags) {
        tags[k].value = Math.abs(tags[k].value);
        total += tags[k].value;
    }
    
    
    
    // Draw the empty pie
    var begin = 0;
    var stop = 0;
    var radius = (canvas.height*0.75)/2;
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.beginPath();
    ctx.shadowBlur = 6;
    ctx.shadowOffsetX = 6;
    ctx.shadowOffsetY = 6;
    ctx.shadowColor = "#555";
    ctx.lineWidth = (opts && opts.hires) ? 6 : 2;
    ctx.strokeStyle = "#222";
    ctx.arc((canvas.width-140)/2,canvas.height/2,radius, 0, Math.PI * 2);
    ctx.closePath();
    ctx.stroke();
    ctx.fill();
    ctx.shadowBlur = 0;
    ctx.shadowOffsetY = 0;
    ctx.shadowOffsetX = 0;
    
    
    // Draw a title if set:
    if (opts && opts.title) {
        ctx.font= (opts && opts.hires) ? "28px Sans-Serif" : "15px Sans-Serif";
        ctx.fillStyle = "#000000";
        ctx.textAlign = "center";
        ctx.fillText(opts.title,(canvas.width-140)/2, (opts && opts.hires) ? 30:15);
        ctx.textAlign = "left";
    }
    
    ctx.beginPath();
    var posY = 50;
    var left = 120 + ((canvas.width-140)/2) + ((opts && opts.hires) ? 40 : 25)
    for (k in tags) {
        var val = tags[k].value;
        stop = stop + (2 * Math.PI * (val / total));
        
        // Make a pizza slice
        ctx.beginPath();
        ctx.lineCap = 'round';
        ctx.arc((canvas.width-140)/2,canvas.height/2,radius,begin,stop);
        ctx.lineTo((canvas.width-140)/2,canvas.height/2);
        ctx.closePath();
        ctx.lineWidth = 0;
        ctx.stroke();
        
        // Add color gradient
        var grd=ctx.createLinearGradient(0,canvas.height*0.2,0,canvas.height);
        grd.addColorStop(0,colors[k % colors.length][1]);
        grd.addColorStop(1,colors[k % colors.length][0]);
        ctx.fillStyle = grd;
        ctx.fill();
        begin = stop;
        
        // Make color legend
        ctx.fillRect(left, posY-((opts && opts.hires) ? 15 : 10), (opts && opts.hires) ? 14 : 7, (opts && opts.hires) ? 14 : 7);
        
        // Add legend text
        ctx.shadowColor = "rgba(0,0,0,0)"
        ctx.font= (opts && opts.hires) ? "22px Sans-Serif" : "12px Sans-Serif";
        ctx.fillStyle = "#000";
        ctx.fillText(tags[k].title + " (" + Math.floor(val) + (opts && opts.pct ? "%" : "") + ")",left+20,posY);
        
        posY += (opts && opts.hires) ? 28 : 14;
    }
    
}


/* Function for drawing line charts
 * Example usage:
 * quokkaLines("myCanvas", ['Line a', 'Line b', 'Line c'], [ [x1,a1,b1,c1], [x2,a2,b2,c2], [x3,a3,b3,c3] ], { stacked: true, curve: false, title: "Some title" } );
 */
function quokkaLines(id, titles, values, options, sums) {
    var canvas = document.getElementById(id);
    var ctx=canvas.getContext("2d");
    // clear the canvas first
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    
    


    ctx.lineWidth = 0.25;
    ctx.strokeStyle = "#000000";
    
    var lwidth = 300;
    var lheight = 75;
    wspace = (options && options.hires) ? 110 : 55;
    var rectwidth = canvas.width - lwidth - wspace;
    var stack = options ? options.stack : false;
    var curve = options ? options.curve : false;
    var title = options ? options.title : null;
    var spots = options ? options.points : false;
    var noX = options ? options.nox : false;
    var verts = options ? options.verts : true;
    if (noX) {
        lheight = 0;
    }
    
    
    // calc rectwidth if titles are large
    var nlwidth = 0
    for (var k in titles) {
        ctx.font= (options && options.hires) ? "24px Sans-Serif" : "12px Sans-Serif";
        ctx.fillStyle = "#00000";
        var x = parseInt(k)
        if (!noX) {
            x = x + 1;
        }
        var sum = 0
        for (var y in values) {
            sum += values[y][x]
        }
        var t = titles[k] + (!options.nosum ? " (" + ((sums && sums[k]) ? sums[k] : sum.toFixed(0)) + ")" : "");
        var w = ctx.measureText(t).width + 48;
        if (w > lwidth && w > nlwidth) {
            nlwidth = w
        }
        if (nlwidth > 0) {
            rectwidth -= nlwidth - lwidth
            lwidth = nlwidth
        }
    }
    
    // Draw a border
    ctx.lineWidth = 0.5;
    ctx.strokeRect((wspace*0.75), 30, rectwidth, canvas.height - lheight - 40);
    
    // Draw a title if set:
    if (title != null) {
        ctx.font= (options && options.hires) ? "24px Sans-Serif" : "15px Sans-Serif";
        ctx.fillStyle = "#00000";
        ctx.textAlign = "center";
        ctx.fillText(title,rectwidth/2, 20);
    }
    
    // Draw legend
    ctx.textAlign = "left";
    var posY = 50;
    for (var k in titles) {
        var x = parseInt(k)
        if (!noX) {
            x = x + 1;
        }
        var sum = 0
        for (var y in values) {
            sum += values[y][x]
        }
        
        var title = titles[k] + (!options.nosum ? (" (" + ((sums && sums[k]) ? sums[k] : sum.toFixed(0)) + ")") : "");
        if (options && options.lastsum) {
            title = titles[k] + " (" + values[values.length-1][x].toFixed(0) + ")";
        }
        ctx.fillStyle = colors[k % colors.length][3];
        ctx.fillRect(wspace + rectwidth + 75 , posY-((options && options.hires) ? 18:9), (options && options.hires) ? 20:10, (options && options.hires) ?20:10);
        
        // Add legend text
        ctx.font= (options && options.hires) ? "24px Sans-Serif" : "14px Sans-Serif";
        ctx.fillStyle = "#00000";
        ctx.fillText(title,canvas.width - lwidth + ((options && options.hires) ? 100:60), posY);
        
        posY += (options && options.hires) ? 30:15;
    }
    
    // Find max and min
    var max = null;
    var min = 0;
    var stacked = null;
    for (x in values) {
        var s = 0;
        for (y in values[x]) {
            if (y > 0 || noX) {
                s += values[x][y];
                if (max === null || max < values[x][y]) {
                    max = values[x][y];
                }
                if (min === null || min > values[x][y]) {
                    min = values[x][y];
                }
            }
        }
        if (stacked === null || stacked < s) {
            stacked = s;
        }
    }
    if (min == max) max++;
    if (stack) {
        min = 0;
        max = stacked;
    }
    
    
    // Set number of lines to draw and each step
    var numLines = 5;
    var step = (max-min) / (numLines+1);
    
    // Prettify the max value so steps aren't ugly numbers
    if (step %1 != 0) {
        step = (Math.round(step+0.5));
        max = step * (numLines+1);
    }
    
    // Draw horizontal lines
    
    for (x = -1; x <= numLines; x++) {
        ctx.beginPath();
        var y = 30 + (((canvas.height-40-lheight) / (numLines+1)) * (x+1));
        ctx.moveTo(wspace*0.75, y);
        ctx.lineTo(wspace*0.75 + rectwidth, y);
        ctx.lineWidth = 0.25;
        ctx.stroke();
        
        // Add values
        ctx.font= (options && options.hires) ? "20px Sans-Serif" : "12px Sans-Serif";
        ctx.fillStyle = "#000000";
        
        var val = Math.round( ((max-min) - (step*(x+1))) );
        if (options && options.traffic) {
            val = quokka_fnmb(val);
        }
        ctx.textAlign = "left";
        ctx.fillText( val,canvas.width - lwidth - 20, y+8);
        ctx.textAlign = "right";
        ctx.fillText( val,wspace-32, y+8);
        ctx.closePath();
    }
    
    
    
    // Draw vertical lines
    var sx = 1
    var numLines = values.length-1;
    var step = (canvas.width - lwidth - wspace*0.75) / values.length;
    while (step < 24) {
        step *= 2
        sx *= 2
    }
    
    
    if (verts) {
        ctx.beginPath();
        for (var x = 1; x < values.length; x++) {
            if (x % sx == 0) {
                var y = (wspace*0.75) + (step * (x/sx));
                ctx.moveTo(y, 30);
                ctx.lineTo(y, canvas.height - 10 - lheight);
                ctx.lineWidth = 0.25;
                ctx.stroke();
            }
        }
        ctx.closePath();
    }
    
    
    
    // Some pre-calculations of steps
    var step = (rectwidth) / (values.length > 1 ? values.length-1:1);
    
    // Draw X values if noX isn't set:
    if (noX != true) {
        ctx.beginPath();
        for (var i = 0; i < values.length; i++) {
            zz = 1
            var x = (wspace*0.75) + ((step) * i);
            var y = canvas.height - lheight + 5;
            if (i % sx == 0) {
                ctx.translate(x, y);
                ctx.moveTo(0,0);
                ctx.lineTo(0,-15);
                ctx.stroke();
                ctx.rotate(45*Math.PI/180);
                ctx.textAlign = "left";
                var val = values[i][0];
                if (val.constructor.toString().match("Date()")) {
                    val = val.toDateString();
                }
                ctx.fillText(val.toString(), 0, 0);
                ctx.rotate(-45*Math.PI/180);
                ctx.translate(-x,-y);
            }
        }
        ctx.closePath();
        
    }
    
    
    
    
    // Draw each line
    var stacks = [];
    var pstacks = [];
    for (k in values) { if (k > 0) { stacks[k] = 0; pstacks[k] = canvas.height - 40 - lheight; }}
    
    for (k in titles) {
        var maxY = 0, minY = 99999;
        ctx.beginPath();
        var color = colors[k % colors.length][0];
        var f = parseInt(k) + 1;
        if (noX) {
            f = parseInt(k);
        }
        var value = values[0][f];
        var step = rectwidth / numLines;
        var x = (wspace*0.75);
        var y = (canvas.height - 10 - lheight) - (((value-min) / (max-min)) * (canvas.height - 40 - lheight));
        var py = y;
        if (stack) {
            stacks[0] = stacks[0] ? stacks[0] : 0
            y -= stacks[0];
            pstacks[0] = stacks[0];
            stacks[0] += (((value-min) / (max-min)) * (canvas.height - 40 - lheight));
        }
        
        // Draw line
        ctx.moveTo(x, y);
        var pvalY = y;
        var pvalX = x;
        for (var i in values) {
            if (i > 0) {
                x = (wspace*0.75) + (step*i);
                var f = parseInt(k) + 1;
                if (noX == true) {
                    f = parseInt(k);
                }
                value = values[i][f];
                y = (canvas.height - 10 - lheight) - (((value-min) / (max-min)) * (canvas.height - 40 - lheight));
                if (stack) {
                    y -= stacks[i];
                    pstacks[i] = stacks[i];
                    stacks[i] += (((value-min) / (max-min)) * (canvas.height - 40- lheight));
                }
                if (y > maxY) maxY = y;
                if (y < minY) minY = y;
                // Draw curved lines??
                /* We'll do: (x1,y1)-----(x1.5,y1)
                 *                          |
                 *                       (x1.5,y2)-----(x2,y2)
                 * with a quadratic beizer thingy
                */
                if (curve) {
                    ctx.bezierCurveTo((pvalX + x) / 2, pvalY, (pvalX + x) / 2, y, x, y);
                    pvalX = x;
                    pvalY = y;
                }
                // Nope, just draw straight lines
                else {
                    ctx.lineTo(x, y);
                }
                if (spots) {
                    ctx.fillStyle = color;
                    ctx.translate(x-2, y-2);
                    ctx.rotate(-45*Math.PI/180);
                    ctx.fillRect(-2,1,4,4);
                    ctx.rotate(45*Math.PI/180);
                    ctx.translate(-x+2, -y+2);
                }
            }
        }
        
        ctx.lineWidth = 4;
        ctx.strokeStyle = color;
        ctx.stroke();
        
        
        if (minY == maxY) maxY++;
        
        // Draw stack area
        if (stack) {
            ctx.globalAlpha = 0.65;
            for (i in values) {
                if (i > 0) {
                    var f = parseInt(k) + 1;
                    if (noX == true) {
                        f = parseInt(k);
                    }
                    x = (wspace*0.75) + (step*i);
                    value = values[i][f];
                    y = (canvas.height - 10 - lheight) - (((value-min) / (max-min)) * (canvas.height - 40 - lheight));
                    y -= stacks[i];
                }
            }
            var pvalY = y;
            var pvalX = x;
            if (y > maxY) maxY = y;
            if (y < minY) minY = y;
            for (i in values) {
                var l = values.length - i - 1;
                x = (wspace*0.75) + (step*l);
                y = canvas.height - 10 - lheight - pstacks[l];
                if (y > maxY) maxY = y;
                if (y < minY) minY = y;
                if (curve) {
                    ctx.bezierCurveTo((pvalX + x) / 2, pvalY, (pvalX + x) / 2, y, x, y);
                    pvalX = x;
                    pvalY = y;
                }
                else {
                    ctx.lineTo(x, y);
                }
            }
            ctx.lineTo((wspace*0.75), py - pstacks[0]);
            ctx.lineWidth = 0;
            var grad = ctx.createLinearGradient(0, minY, 0, maxY);
            grad.addColorStop(0.25, colors[k % colors.length][0])
            grad.addColorStop(1, colors[k % colors.length][1])
            ctx.strokeStyle = colors[k % colors.length][0];
            ctx.fillStyle = grad;
            ctx.fill();
            ctx.fillStyle = "#000"
            ctx.strokeStyle = "#000"
            ctx.globalAlpha = 1;
        }
        ctx.closePath();
    }
    
    // draw feather
    base_image = new Image();
    base_image.src = 'data:image/png;base64,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';
    base_image.onload = function(){
        ctx.globalAlpha = 0.15
        ctx.drawImage(base_image, (canvas.width/2) - 64 - (lwidth/2), (canvas.height/2) - 128);
        ctx.globalAlpha = 1
    }
}



/* Function for drawing line charts
 * Example usage:
 * quokkaLines("myCanvas", ['Line a', 'Line b', 'Line c'], [ [x1,a1,b1,c1], [x2,a2,b2,c2], [x3,a3,b3,c3] ], { stacked: true, curve: false, title: "Some title" } );
 */
function quokkaBars(id, titles, values, options) {
    var canvas = document.getElementById(id);
    var ctx=canvas.getContext("2d");
    // clear the canvas first
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    var lwidth = 150;
    var lheight = 75;
    var stack = options ? options.stack : false;
    var astack = options ? options.astack : false;
    var curve = options ? options.curve : false;
    var title = options ? options.title : null;
    var noX = options ? options.nox : false;
    var verts = options ? options.verts : true;
    if (noX) {
        lheight = 0;
    }
    
    
    
    // Draw a border
    ctx.lineWidth = 0.5;
    ctx.strokeRect(25, 30, canvas.width - lwidth - 40, canvas.height - lheight - 40);
    
    // Draw a title if set:
    if (title != null) {
        ctx.font="15px Arial";
        ctx.fillStyle = "#000";
        ctx.textAlign = "center";
        ctx.fillText(title,(canvas.width-lwidth)/2, 15);
    }
    
    // Draw legend
    ctx.textAlign = "left";
    var posY = 50;
    for (var k in titles) {
        var x = parseInt(k)
        if (!noX) {
            x = x + 1;
        }
        var title = titles[k];
        if (title && title.length > 0) {
            ctx.fillStyle = colors[k % colors.length][0];
            ctx.fillRect(canvas.width - lwidth + 20, posY-10, 10, 10);
            
            // Add legend text
            ctx.font="12px Arial";
            ctx.fillStyle = "#000";
            ctx.fillText(title,canvas.width - lwidth + 40, posY);
            
            posY += 15;
        }
        

    }
    
    // Find max and min
    var max = null;
    var min = 0;
    var stacked = null;
    for (x in values) {
        var s = 0;
        for (y in values[x]) {
            if (y > 0 || noX) {
                s += values[x][y];
                if (max == null || max < values[x][y]) {
                    max = values[x][y];
                }
                if (min == null || min > values[x][y]) {
                    min = values[x][y];
                }
            }
        }
        if (stacked == null || stacked < s) {
            stacked = s;
        }
    }
    if (min == max) {
        max++;
    }
    if (stack) {
        min = 0;
        max = stacked;
    }
    
    
    // Set number of lines to draw and each step
    var numLines = 5;
    var step = (max-min) / (numLines+1);
    
    // Prettify the max value so steps aren't ugly numbers
    if (step %1 != 0) {
        step = (Math.round(step+0.5));
        max = step * (numLines+1);
    }
    
    // Draw horizontal lines
    for (x = numLines; x >= 0; x--) {
        
        var y = 30 + (((canvas.height-40-lheight) / (numLines+1)) * (x+1));
        ctx.moveTo(25, y);
        ctx.lineTo(canvas.width - lwidth - 15, y);
        ctx.lineWidth = 0.25;
        ctx.stroke();
        
        // Add values
        ctx.font="10px Arial";
        ctx.fillStyle = "#000";
        ctx.textAlign = "right";
        ctx.fillText( Math.round( ((max-min) - (step*(x+1))) * 100 ) / 100,canvas.width - lwidth + 12, y-4);
        ctx.fillText( Math.round( ((max-min) - (step*(x+1))) * 100 ) / 100,20, y-4);
    }
    
    
    // Draw vertical lines
    var sx = 1
    var numLines = values.length-1;
    var step = (canvas.width - lwidth - 40) / values.length;
    while (step < 24) {
        step *= 2
        sx *= 2
    }
    
    
    if (verts) {
        ctx.beginPath();
        for (var x = 1; x < values.length; x++) {
            if (x % sx == 0) {
                var y = 35 + (step * (x/sx));
                ctx.moveTo(y, 30);
                ctx.lineTo(y, canvas.height - 10 - lheight);
                ctx.lineWidth = 0.25;
                ctx.stroke();
            }
        }
    }
    
    
    
    // Some pre-calculations of steps
    var step = (canvas.width - lwidth - 48) / values.length;
    var smallstep = (step / titles.length) - 2;
    
    // Draw X values if noX isn't set:
    if (noX != true) {
        ctx.beginPath();
        for (var i = 0; i < values.length; i++) {
            smallstep = (step / (values[i].length-1)) - 2;
            zz = 1
            var x = 35 + ((step) * i);
            var y = canvas.height - lheight + 5;
            if (i % sx == 0) {
                ctx.translate(x, y);
                ctx.moveTo(0,0);
                ctx.lineTo(0,-15);
                ctx.stroke();
                ctx.rotate(45*Math.PI/180);
                ctx.textAlign = "left";
                var val = values[i][0];
                if (val.constructor.toString().match("Date()")) {
                    val = val.toDateString();
                }
                ctx.fillText(val.toString(), 0, 0);
                ctx.rotate(-45*Math.PI/180);
                ctx.translate(-x,-y);
            }
        }
        
    }
    
    
    
    
    // Draw each line
    var stacks = [];
    var pstacks = [];
    
    for (k in values) {
        smallstep = (step / (values[k].length)) - 2;
        stacks[k] = 0;
        pstacks[k] = canvas.height - 40 - lheight;
        var beginX = 0;
        for (i in values[k]) {
            if (i > 0 || noX) {
                var z = parseInt(i);
                var zz = z;
                if (!noX) {
                    z = parseInt(i) + 1;
                    zz = z - 2;
                    if (z > values[k].length) {
                        break;
                    }
                }
                var value = values[k][i];
                var title = titles[i];
                var color = colors[zz % colors.length][1];
                var fcolor = colors[zz % colors.length][2];
                if (values[k][2] && values[k][2].toString().match(/^#.+$/)) {
                    color = values[k][2]
                    fcolor = values[k][2]
                    smallstep = (step / (values[k].length-2)) - 2;
                }
                var x = ((step) * k) + ((smallstep+2) * zz) + 5;
                var y = canvas.height - 10 - lheight;
                var mdiff = (max-min);
                mdiff = (mdiff == 0) ? 1 : mdiff;
                var height = ((canvas.height - 40 - lheight) / (mdiff)) * value * -1;
                var width = smallstep - 2;
                if (width <= 1) {
                    width = 1
                }
                if (stack) {
                    width = step - 10;
                    y -= stacks[k];
                    stacks[k] -= height;
                    x = (step * k) + 4;
                    if (astack) {
                        y = canvas.height - 10 - lheight;
                    }
                }
                
                        
                // Draw bar
                ctx.beginPath();
                ctx.lineWidth = 2;
                ctx.strokeStyle = color;
                ctx.strokeRect(27 + x, y, width, height);
                var alpha = 0.75
                if (fcolor.r) {
                    ctx.fillStyle = 'rgba('+ [parseInt(fcolor.r*255),parseInt(fcolor.g*255),parseInt(fcolor.b*255),alpha].join(",") + ')';
                } else {
                    ctx.fillStyle = fcolor;
                }
                ctx.fillRect(27 + x, y, width, height);
                
            }
        }
        

    }
}


]==]


status_css = [[
    html {
    font-size: 14px;
    position: relative;
    background: #272B30;
    }

    body {
        background-color: #272B30;
        color: #000;
        margin: 0 auto;
        min-height: 100%;
        font-family: Arial, Helvetica, sans-serif;
        font-weight: normal;
    }
    
    .navbarLeft {
        background: linear-gradient(to bottom, #F8A900 0%,#D88900 100%);
        width: 200px;
        height: 30px;
        padding-top: 2px;
        font-size: 1.35rem;
        color: #FFF;
        border-bottom: 2px solid #000;
        float: left;
        text-align: center;
    }
    
    .navbarRight {
        background: linear-gradient(to bottom, #EFEFEF 0%,#EEE 100%);
        width: calc(100% - 240px);
        height: 28px;
        color: #333;
        border-bottom: 2px solid #000;
        float: left;
        font-size: 1.3rem;
        padding-top: 4px;
        text-align: left;
        padding-left: 40px;
    }
    
    .wrapper {
        width: 100%;
        float: left;
        background: #33363F;
        min-height: calc(100% - 80px);
        position: relative;
    }
    
    .serverinfo {
        float: left;
        width: 200px;
        height: calc(100% - 34px);
        background: #293D4C;
    }
    
    .skey {
        background: rgba(30,30,30,0.3);
        color: #C6E7FF;
        font-weight: bold;
        padding: 2px;
    }
    
    .sval {
        padding: 2px;
        background: rgba(30,30,30,0.3);
        color: #FFF;
        font-size: 0.8rem;
        border-bottom: 1px solid rgba(200,200,200,0.2);
    }
    
    .charts {
        padding: 0px;
        width: calc(100% - 220px);
        max-width: 1000px;
        min-height: 100%;
        margin: 0px auto;
        position: relative;
        float: left;
        margin-left: 20px;
    }

    pre, code {
        font-family: "Courier New", Courier, monospace;
    }

    strong {
        font-weight: bold;
    }

    q, em, var {
        font-style: italic;
    }
    /* h1                     */
    /* ====================== */
    h1 {
        padding: 0.2em;
        margin: 0;
        border: 1px solid #405871;
        background-color: inherit;
        color: #036;
        text-decoration: none;
        font-size: 22px;
        font-weight: bold;
    }

    /* h2                     */
    /* ====================== */
    h2 {
        padding: 0.2em 0 0.2em 0.7em;
        margin: 0 0 0.5em 0;
        text-decoration: none;
        font-size: 18px;
        font-weight: bold;
        text-align: center;
    }

    #modules {
        margin-top:20px;
        display:none;
        width:400px;
    }
    
    .servers {
        
        width: 1244px;
        background: #EEE;
    }

    tr:nth-child(odd) {
        background: #F6F6F6;
    }
    tr:nth-child(even) {
        background: #EBEBEB;
    }
    td {
        padding: 2px;
    }
    table {
        border: 1px solid #333;
        padding: 0px;
        margin: 5px;
        min-width: 360px;
        background: #999;
        font-size: 0.8rem;
    }
    
    canvas {
        background: #FFF;
        margin: 3px;
        text-align: center;
        padding: 2px;
        border-radius: 10px;
        border: 1px solid #999;
    }
    
    .canvas_wide {
        position: relative;
        width: 65%;
    }
    .canvas_narrow {
        position: relative;
        width: 27%;
    }
    
    a {
        color: #FFA;
    }
    
    .statsbox {
        border-radius: 3px;
        background: #3C3E47;
        min-width: 150px;
        height: 60px;
        float: left;
        margin: 15px;
        padding: 10px;
    }
    
    .btn {
        background: linear-gradient(to bottom, #72ca72 0%,#55bf55 100%);
        border-radius: 5px;
        color: #FFF;
        text-decoration: none;
        padding-top: 6px;
        padding-bottom: 6px;
        padding-left: 3px;
        padding-right: 3px;
        font-weight: bold;
        text-shadow: 1px 1px rgba(0,0,0,0.4);
        margin: 12px;
        float: left;
        clear: none;
    }
    
    .infobox_wrapper {
        float: left;
        min-width: 200px;
        margin: 10px;
    }
    .infobox_title {
        border-top-left-radius: 4px;
        border-top-right-radius: 4px;
        background: #FAB227;
        color: #FFF;
        border: 2px solid #FAB227;
        border-bottom: none;
        font-weight: bold;
        text-align: center;
        width: 100%;
    }
    .infobox {
        background: #222222;
        border: 2px solid #FAB227;
        border-top: none;
        color: #EFEFEF;
        border-bottom-left-radius: 4px;
        border-bottom-right-radius: 4px;
        float: left;
        width: 100%;

    }
    
    
    .serverinfo ul {
        margin: 0px;
        padding: 0px;
        margin-top: 20px;
        list-style: none;
    }
    
    .serverinfo ul li .btn {
        width: calc(100% - 8px);
        margin: 0px;
        border: 0px;
        border-radius: 0px;
        padding: 0px;
        padding-top: 8px;
        padding-left: 8px;
        height: 24px;
        background: rgba(0,0,0,0.2);
        border-bottom: 1px solid rgba(100,100,100,0.3);
    }
    
    .serverinfo  ul li:nth-child(1)  {
        border-top: 1px solid rgba(100,100,100,0.3);
    }
    .serverinfo ul li .btn.active {
        background: rgba(30,30,50,0.2);
        border-left: 4px solid #27FAB2;
        padding-left: 4px;
        color: #FFE;
    }
    
    .serverinfo ul li .btn:hover {
        background: rgba(50,50,50,0.15);
        border-left: 4px solid #FAB227;
        padding-left: 4px;
        color: #FFE;
    }
]]