You are viewing a plain text version of this content. The canonical link for it is here.
Posted to cvs@httpd.apache.org by hu...@apache.org on 2017/03/15 14:21:42 UTC

svn commit: r1787053 - in /httpd/httpd/trunk/docs/server-status: ./ README.md feather.png server-status.lua

Author: humbedooh
Date: Wed Mar 15 14:21:42 2017
New Revision: 1787053

URL: http://svn.apache.org/viewvc?rev=1787053&view=rev
Log:
donating lua server-status script, enjoy!

Added:
    httpd/httpd/trunk/docs/server-status/
    httpd/httpd/trunk/docs/server-status/README.md
    httpd/httpd/trunk/docs/server-status/feather.png   (with props)
    httpd/httpd/trunk/docs/server-status/server-status.lua

Added: httpd/httpd/trunk/docs/server-status/README.md
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/docs/server-status/README.md?rev=1787053&view=auto
==============================================================================
--- httpd/httpd/trunk/docs/server-status/README.md (added)
+++ httpd/httpd/trunk/docs/server-status/README.md Wed Mar 15 14:21:42 2017
@@ -0,0 +1,40 @@
+server-status
+=============
+
+`mod_lua` version of the Apache httpd's mod_status using dynamic charts
+
+## What does it do? ##
+This script is an extended version of the known mod_status statistics page for httpd.
+It uses the simple Quokka Chart API to visualize many of the elements that are sometimes hard 
+to properly diagnose using plain text information.
+
+Take a look at https://www.apache.org/server-status to see how it works.
+
+## Requirements ##
+* Apache httpd 2.4.6 or higher
+* mod_lua (with either Lua 5.1, 5.2 or LuaJIT)
+* mod_status loaded (for enabling traffic statistics)
+
+## Installing ##
+First, install mod_lua (you can enable this during configure time with --enable-lua)
+
+### Installing as a handler:
+To install it as a handler, add the following to your httpd.conf in the appropriate VirtualHost:
+
+    LuaMapHandler ^/server-status$ /path/to/server-status.lua
+    
+### Installing as a web app:
+To install as a plain web-app, enable .lua scripts to be handled by mod_lua, by adding the following 
+to your appropriate VirtualHost configuration:
+
+    AddHandler lua-script .lua
+
+Then just put the `.lua` script somewhere in your document root and visit the page.
+
+## Configuring
+There are a few options inside the Lua script that can be set to `true` or `false`:
+
+- `show_warning`: Whether or not to show a notice that this page is there on purpose.
+- `redact_ips`: Whether or not to replace the last few bits of every IP with 'x.x'
+- `show_modules`: Whether to show the list of loaded modules or not
+- `show_threads`: Whether to show thread details or not.

Added: httpd/httpd/trunk/docs/server-status/feather.png
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/docs/server-status/feather.png?rev=1787053&view=auto
==============================================================================
Binary file - no diff available.

Propchange: httpd/httpd/trunk/docs/server-status/feather.png
------------------------------------------------------------------------------
    svn:mime-type = application/octet-stream

Added: httpd/httpd/trunk/docs/server-status/server-status.lua
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/docs/server-status/server-status.lua?rev=1787053&view=auto
==============================================================================
--- httpd/httpd/trunk/docs/server-status/server-status.lua (added)
+++ httpd/httpd/trunk/docs/server-status/server-status.lua Wed Mar 15 14:21:42 2017
@@ -0,0 +1,1901 @@
+--[[
+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;
+    }
+]]
\ No newline at end of file