]> granicus.if.org Git - apache/blob - docs/server-status/server-status.lua
Makefile.in: merge typo fix from test-integration branch
[apache] / docs / server-status / server-status.lua
1 --[[
2 Licensed to the Apache Software Foundation (ASF) under one
3 or more contributor license agreements.  See the NOTICE file
4 distributed with this work for additional information
5 regarding copyright ownership.  The ASF licenses this file
6 to you under the Apache License, Version 2.0 (the
7 "License"); you may not use this file except in compliance
8 with the License.  You may obtain a copy of the License at
9
10     http://www.apache.org/licenses/LICENSE-2.0
11
12 Unless required by applicable law or agreed to in writing,
13 software distributed under the License is distributed on an
14 "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 KIND, either express or implied.  See the License for the
16 specific language governing permissions and limitations
17 under the License.
18 ]]
19
20 --[[ mod_lua implementation of the server-status page ]]
21 local ssversion = "0.11" -- verion of this script
22 local redact_ips = true -- whether to replace the last two bits of every IP with 'x.x'
23 local warning_banner = [[
24     <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;">
25         <h3 style="margin: 4px; font-size: 1rem;">Don't be alarmed - this page is here for a reason!</h3>
26         <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>
27     </div>
28 ]]
29 local show_warning = true -- whether to display the above warning/notice on the page
30 local show_modules = false -- Whether to list loaded modules or not
31 local show_threads = true -- whether to list thread information or not
32
33 -- pre-declare some variables defined at the bottom of this script:
34 local status_js, status_css, quokka_js
35
36 -- quick and dirty JSON conversion
37 local function quickJSON(input)
38     if type(input) == "table" then
39         local t = 'array'
40         for k, v in pairs(input) do
41             if type(k) ~= "number" then
42                 t = 'hash'
43                 break
44             end
45         end
46         
47         if t == 'hash' then
48             local out = ""
49             local tbl = {}
50             for k, v in pairs(input) do
51                 local kv = ([["%s": %s]]):format(k, quickJSON(v))
52                 table.insert(tbl, kv)
53             end
54             return "{" .. table.concat(tbl, ", ") .. "}"
55         else
56             local tbl = {}
57             for k, v in pairs(input) do
58                 table.insert(tbl, quickJSON(v))
59             end
60             return "[" .. table.concat(tbl, ", ") .. "]"
61         end
62     elseif type(input) == "string" then
63         return ([["%s"]]):format(input:gsub('"', '\\"'):gsub("[\r\n\t]", " "))
64     elseif type(input) == "number" then
65         return tostring(input)
66     elseif type(input) == "boolean" then
67         return (input and "true" or "false")
68     else
69         return "null"
70     end
71 end
72
73 -- Module information callback
74 local function modInfo(r, modname)
75     if modname then
76             r:puts [[
77     <!DOCTYPE html>
78     <html>
79       <head>
80         <meta charset="utf-8">
81         <style>
82         ]]
83         r:puts (status_css)
84         r:puts [[
85         </style>
86         <title>Module information</title>
87       </head>
88     
89       <body>
90     ]]
91         r:puts( ("<h3>Details for module %s</h3>\n"):format(r:escape_html(modname)) )
92         -- Queries the server for information about a module
93         local mod = r.module_info(modname)
94         if mod then
95             for k, v in pairs(mod.commands) do
96                 -- print out all directives accepted by this module
97                 r:puts( ("<b>%s:</b> %s<br>\n"):format(r:escape_html(k), v))
98             end
99         end
100         -- HTML tail
101         r:puts[[
102       </body>
103     </html>
104     ]]
105     end
106 end
107
108 -- Function for generating server stats
109 function getServerState(r, verbose)
110     local state = {}
111     
112     state.mpm = {
113         type = "prefork", -- default to prefork until told otherwise
114         threadsPerChild = 1,
115         threaded = false,
116         maxServers = r.mpm_query(12),
117         activeServers = 0
118     }
119     if r.mpm_query(14) == 1 then
120         state.mpm.type = "event" -- this is event mpm
121     elseif r.mpm_query(3) >= 1 then
122         state.mpm.type = "worker" -- it's not event, but it's threaded, we'll assume worker mpm (could be motorz??)
123     elseif r.mpm_query(2) == 1 then
124         state.mpm.type = "winnt" -- it's threaded, but not worker nor event, so it's probably winnt
125     end
126     if state.mpm.type ~= "prefork" then
127         state.mpm.threaded = true -- it's threaded
128         state.mpm.threadsPerChild = r.mpm_query(6) -- get threads per child proc
129     end
130     
131     state.processes = {} -- list of child procs
132     state.connections = { -- overall connection info
133         idle = 0,
134         active = 0
135     }
136     -- overall server stats
137     state.server = {
138         connections = 0,
139         bytes = 0,
140         built = r.server_built,
141         localtime = os.time(),
142         uptime = os.time() - r.started,
143         version = r.banner,
144         host = r.server_name,
145         modules = nil,
146         extended = show_threads, -- whether extended status is available or not
147     }
148     
149     -- if show_modules is true, add list of modules to the JSON
150     if show_modules then
151         state.server.modules = {}
152         for k, module in pairs(r:loaded_modules()) do
153             table.insert(state.server.modules, module)
154         end
155     end
156     
157     -- Fetch process/thread data
158     for i=0,state.mpm.maxServers-1,1 do
159         local server = r.scoreboard_process(r, i);
160         if server then
161             local s = {
162                 active = false,
163                 pid = nil,
164                 bytes = 0,
165                 stime = 0,
166                 utime = 0,
167                 connections = 0,
168             }
169             local tstates = {}
170             if server.pid then
171                 state.connections.idle = state.connections.idle + (server.keepalive or 0)
172                 s.connections = 0
173                 if server.pid > 0 then
174                     state.mpm.activeServers = state.mpm.activeServers + 1
175                     s.active = true
176                     s.pid = server.pid
177                 end
178                 for j = 0, state.mpm.threadsPerChild-1, 1 do
179                     local worker = r.scoreboard_worker(r, i, j)
180                     if worker then
181                         s.stime = s.stime + (worker.stimes or 0);
182                         s.utime = s.utime + (worker.utimes or 0);
183                         if verbose and show_threads then
184                             s.threads = s.threads or {}
185                             table.insert(s.threads, {
186                                 bytes = worker.bytes_served,
187                                 thread = ("0x%x"):format(worker.tid),
188                                 client = redact_ips and (worker.client or "???"):gsub("[a-f0-9]+[.:]+[a-f0-9]+$", "x.x") or worker.client or "???",
189                                 cost = ((worker.utimes or 0) + (worker.stimes or 0)),
190                                 count = worker.access_count,
191                                 vhost = worker.vhost:gsub(":%d+", ""),
192                                 request = worker.request,
193                                 last_used = math.floor(worker.last_used/1000000)
194                             })
195                         end
196                         state.server.connections = state.server.connections + worker.access_count
197                         s.bytes = s.bytes + worker.bytes_served
198                         s.connections = s.connections + worker.access_count
199                         if server.pid > 0 then
200                             tstates[worker.status] = (tstates[worker.status] or 0) + 1
201                         end
202                     end
203                 end
204             end
205             
206             s.workerStates = {
207                 keepalive = (server.keepalive > 0) and server.keepalive or tstates[5] or 0,
208                 closing = tstates[8] or 0,
209                 idle = tstates[2] or 0,
210                 writing = tstates[4] or 0,
211                 reading = tstates[3] or 0,
212                 graceful = tstates[9] or 0
213             }
214             table.insert(state.processes, s)
215             state.server.bytes = state.server.bytes + s.bytes
216             state.connections.active = state.connections.active + (tstates[8] or 0) + (tstates[4] or 0) + (tstates[3] or 0)
217         end
218     end
219     return state
220 end
221
222 -- Handler function
223 function handle(r)
224     
225     -- Parse GET data, if any, and set content type
226     local GET = r:parseargs()
227     
228     if GET['module'] then
229         modInfo(r, GET['module'])
230         return apache2.OK
231     end
232
233
234     -- If we only need the stats feed, compact it and hand it over
235     if GET['view'] and GET['view'] == "json" then
236         local state = getServerState(r, GET['extended'] == 'true')
237         r.content_type = "application/json"
238         r:puts(quickJSON(state))
239         return apache2.OK
240     end
241     
242     if not GET['resource'] then
243     
244         local state = getServerState(r, show_threads)
245         
246         -- Print out the HTML for the front page
247         r.content_type = "text/html"
248         r:puts ( ([=[
249     <!DOCTYPE html>
250     <html>
251       <head>
252         <meta charset="utf-8">
253         <!-- Stylesheet -->
254         <link href="?resource=css" rel="stylesheet">
255         
256         <!-- JavaScript-->
257         <script type="text/javascript" src="?resource=js"></script>
258         
259         <title>Server status for %s</title>
260       </head>
261     
262       <body onload="refreshCharts(false);">
263         <div class="wrapper" id="wrapper">
264             <div class="navbarLeft">
265                 <img align='absmiddle' src='?resource=feather' width="15" height="30"/>
266                 Apache HTTPd
267             </div>
268             <div class="navbarRight">Status for %s on %s</div>
269             <div style="clear: both;"></div>
270             <div class="serverinfo" id="leftpane">
271                 <ul id="menubar">
272                     <li>
273                         <a class="btn active" id="dashboard_button" href="javascript:void(showPanel('dashboard'));">Dashboard</a>
274                     </li>
275                     <li>
276                         <a class="btn" id="misc_button" href="javascript:void(showPanel('misc'));">Server Info</a>
277                     </li>
278                     <li>
279                         <a class="btn" id="threads_button" style="display: none;" href="javascript:void(showPanel('threads'));">Show thread information</a>
280                     </li>
281                     <li>
282                         <a class="btn" id="modules_button" style="display: none;" href="javascript:void(showPanel('modules'));">Show loaded modules</a>
283                     </li>
284                 </ul>
285                 
286                 <!-- warning --> %s <!-- /warning -->
287                 
288             </div>
289             
290             <!-- dashboard -->
291             <div class="charts" id="dashboard_panel">
292             
293                 <div class="infobox_wrapper" style="clear: both; width: 100%%;">
294                     <div class="infobox_title">Quick Stats</div>
295                     <div class="infobox" id="general_stats">
296                     </div>
297                 </div>
298                 <div class="infobox_wrapper" style="width: 100%%;">
299                     <div class="infobox_title">Charts</div>
300                     <div class="infobox">
301                         <!--Div that will hold the pie chart-->
302                         <canvas id="actions_div" width="1400" height="400" class="canvas_wide"></canvas>
303                         <canvas id="status_div" width=580" height="400" class="canvas_narrow"></canvas>
304                         <canvas id="traffic_div" width="1400" height="400" class="canvas_wide"></canvas>
305                         <canvas id="idle_div" width="580" height="400" class="canvas_narrow"></canvas>
306                         <canvas id="connection_div" width="1400" height="400" class="canvas_wide"></canvas>
307                         <canvas id="cpu_div" width="580" height="400" class="canvas_narrow"></canvas>
308                         <div style="clear: both"></div>
309                     </div>
310                 </div>
311             </div>
312             
313             <!-- misc server info -->
314             <div class="charts" id="misc_panel" style="display: none;">
315                 <div class="infobox_wrapper" style="clear: both; width: 100%%;">
316                     <div class="infobox_title">General server information</div>
317                     <div class="infobox" style='padding: 16px; width: calc(100%% - 32px);' id="server_breakdown">
318                     </div>
319                 </div>
320             </div>
321             
322             <!-- thread info -->
323             <div class="charts" id="threads_panel" style="display: none;">
324                 <div class="infobox_wrapper" style="clear: both; width: 100%%;">
325                     <div class="infobox_title">Thread breakdown</div>
326                     <div class="infobox" style='padding: 16px; width: calc(100%% - 32px);' id="threads_breakdown">
327                     </div>
328                 </div>
329             </div>
330             
331             <!-- module info -->
332             <div class="charts" id="modules_panel" style="display: none;">
333                 <div class="infobox_wrapper" style="clear: both; width: 100%%;">
334                     <div class="infobox_title">Modules loaded</div>
335                     <div class="infobox" style='padding: 16px; width: calc(100%% - 32px);' id="modules_breakdown">
336                     blabla
337                     </div>
338                 </div>
339             </div>
340             
341             
342         </div>
343     
344     
345     ]=]):format(
346         r.server_name,
347         r.banner,
348         r.server_name,
349         show_warning and warning_banner or ""
350         ) );
351         -- HTML tail
352         r:puts[[
353         </body>
354       </html>
355       ]]
356     else
357         -- Resource documents (CSS, JS, PNG)
358         if GET['resource'] == 'js' then
359             r.content_type = "application/javascript"
360             r:puts(quokka_js)
361             r:puts(status_js)
362         elseif GET['resource'] == 'css' then
363             r.content_type = "text/css"
364             r:puts(status_css)
365         elseif GET['resource'] == 'feather' then
366             r.content_type = "image/png"
367             r:write(r:base64_decode('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'))
368         end
369     end
370     return apache2.OK;
371 end
372
373
374 ------------------------------------
375 -- JavaScript and CSS definitions --
376 ------------------------------------
377
378 -- Set up some JavaScripts:
379 status_js = [==[
380 Number.prototype.pad = function(size) {
381     var str = String(this);
382     while (str.length < size) {
383         str = "0" + str;
384     }
385     return str;
386 }
387
388 function getAsync(theUrl, xstate, callback) {
389     var xmlHttp = null;
390     if (window.XMLHttpRequest) {
391         xmlHttp = new XMLHttpRequest();
392     } else {
393         xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
394     }
395     xmlHttp.open("GET", theUrl, true);
396     xmlHttp.send(null);
397     xmlHttp.onreadystatechange = function(state) {
398         if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {
399             if (callback) {
400                 callback(JSON.parse(xmlHttp.responseText));
401             }
402             
403         }
404     }
405 }
406
407 var actionCache = [];
408 var connectionCache = [];
409 var trafficCache = [];
410 var processes = {};
411 var lastBytes = 0;
412 var lastConnections = 0;
413 var negativeBytes = 0; // cache for proc reloads, which skews traffic
414 var updateSpeed = 5; // How fast do charts update?
415 var maxRecords = 24; // How many records to show per chart
416 var cpumax = 1000000; // random cpu max(?)
417
418 function refreshCharts(json, state) {
419     if (json && json.processes) {
420         
421         
422         
423          // general server info box
424         var gs = document.getElementById('server_breakdown');
425         gs.innerHTML = "";
426         gs.innerHTML += "<b>Server version: </b>" + json.server.version + "<br/>";
427         gs.innerHTML += "<b>Server built: </b>" + json.server.built + "<br/>";
428         gs.innerHTML += "<b>Server MPM: </b>" + json.mpm.type + " <span id='mpminfo'></span><br/>";
429         
430         
431         // Get a timestamp
432         var now = new Date();
433         var ts = now.getHours().pad(2) + ":" + now.getMinutes().pad(2) + ":" + now.getSeconds().pad(2);
434         
435         var utime = 0;
436         var stime = 0;
437         
438         // Construct state based on proc details
439         var state = {
440             timestamp: ts,
441             closing: 0,
442             idle: 0,
443             writing: 0,
444             reading: 0,
445             keepalive: 0,
446             graceful: 0
447         }
448         for (var i in json.processes) {
449             var proc = json.processes[i];
450             if (proc.pid) {
451                 state.closing += proc.workerStates.closing||0;
452                 state.idle += proc.workerStates.idle||0;
453                 state.writing += proc.workerStates.writing||0;
454                 state.reading += proc.workerStates.reading||0;
455                 state.keepalive += proc.workerStates.keepalive||0;
456                 state.graceful += proc.workerStates.graceful||0;
457                 utime += proc.utime;
458                 stime += proc.stime;
459             }
460         }
461         
462         // Push action state entry into action cache with timestamp
463         // Shift if more than 10 entries in cache
464         actionCache.push(state);
465         if (actionCache.length > maxRecords) {
466             actionCache.shift();
467         }
468         
469         // construct array for QuokkaLines
470         var arr = [];
471         for (var i in actionCache) {
472             var el = actionCache[i];
473             if (json.mpm.type == 'event') {
474             arr.push([el.timestamp, el.closing, el.idle, el.writing, el.reading, el.graceful]);
475             } else {
476                 arr.push([el.timestamp, el.keepalive, el.closing, el.idle, el.writing, el.reading, el.graceful]);
477             }
478         }
479         var states = ['Keepalive', 'Closing', 'Idle', 'Writing', 'Reading', 'Graceful']
480         if (json.mpm.type == 'event') {
481             states.shift();
482             if (document.getElementById('mpminfo')) {
483                 document.getElementById('mpminfo').innerHTML = "(" + fn(parseInt(json.connections.idle)) + " connections in idle keepalive)";
484             }
485         }
486         // Draw action chart
487         quokkaLines("actions_div", states, arr, { lastsum: true, hires: true, nosum: true, stack: true, curve: true, title: "Thread states" } );
488         
489         
490         // Get traffic, figure out how much it was this time (0 if just started!)
491         var bytesThisTurn = 0;
492         var connectionsThisTurn = 0;
493         for (var i in json.processes) {
494             var proc = json.processes[i];
495             var pid = proc.pid
496             // if we haven't seen this proc before, ignore its bytes first time
497             if (!processes[pid]) {
498                 processes[pid] = {
499                     bytes: proc.bytes,
500                     connections: proc.connections,
501                 }
502             } else {
503                 bytesThisTurn += proc.bytes - processes[pid].bytes;
504                 if (pid) {
505                     x = proc.connections - processes[pid].connections;
506                     connectionsThisTurn += (x > 0) ? x : 0;
507                 }
508                 processes[pid].bytes = proc.bytes;
509                 processes[pid].connections = proc.connections;
510             }
511         }
512         
513         if (lastBytes == 0 ) {
514             bytesThisTurn = 0;
515         }
516         lastBytes = 1;
517
518         // Push a new element into cache, prune cache
519         var el = {
520             timestamp: ts,
521             bytes: bytesThisTurn/updateSpeed
522         };
523         trafficCache.push(el);
524         if (trafficCache.length > maxRecords) {
525             trafficCache.shift();
526         }
527         
528         // construct array for QuokkaLines
529         arr = [];
530         for (var i in trafficCache) {
531             var el = trafficCache[i];
532             arr.push([el.timestamp, el.bytes]);
533         }
534         // Draw action chart
535         quokkaLines("traffic_div", ['Traffic'], arr, { traffic: true, hires: true, nosum: true, stack: true, curve: true, title: "Traffic per second" } );
536         
537         
538         // Get connections per second
539         // Push a new element into cache, prune cache
540         var el = {
541             timestamp: ts,
542             connections: (connectionsThisTurn+1)/updateSpeed
543         };
544         connectionCache.push(el);
545         if (connectionCache.length > maxRecords) {
546             connectionCache.shift();
547         }
548         
549         // construct array for QuokkaLines
550         arr = [];
551         for (var i in connectionCache) {
552             var el = connectionCache[i];
553             arr.push([el.timestamp, el.connections]);
554         }
555         // Draw connection chart
556         quokkaLines("connection_div", ['Connections/sec'], arr, { traffic: false, hires: true, nosum: true, stack: true, curve: true, title: "Connections per second" } );
557         
558         
559         // Thread info
560         quokkaCircle("status_div", [
561         { title: 'Active', value: (json.mpm.threadsPerChild*json.mpm.activeServers)},
562         { title: 'Reserve', value: (json.mpm.threadsPerChild*(json.mpm.activeServers-json.mpm.maxServers))}
563         ],
564             { title: "Worker pool", hires: true});
565         
566         // Idle vs active connections
567         var idlecons = json.connections.idle;
568         var activecons = json.connections.active;
569         quokkaCircle("idle_div", [
570             { title: 'Idle', value: idlecons},
571             { title: 'Active', value: activecons},
572             ],
573             { hires: true, title: "Idle vs active connections"});
574         
575         
576         // CPU info
577         while ( (stime+utime) > cpumax ) {
578             cpumax = cpumax * 2;
579         }
580
581         quokkaCircle("cpu_div", [
582             { title: 'Idle', value: (cpumax - stime - utime) / (cpumax/100)},
583             { title: 'System', value: stime/(cpumax/100)},
584             { title: 'User', value: utime/(cpumax/100)}
585             ],
586             { hires: true, title: "CPU usage", pct: true});
587         
588         
589         
590         
591         
592         
593         // General stats infobox
594         var gstats = document.getElementById('general_stats');
595         gstats.innerHTML = ''; // wipe the box
596         
597             // Days since restart
598             var u_f = Math.floor(json.server.uptime/8640.0) / 10;
599             var u_d = Math.floor(json.server.uptime/86400);
600             var u_h = Math.floor((json.server.uptime%86400)/3600);
601             var u_m = Math.floor((json.server.uptime%3600)/60);
602             var u_s = Math.floor(json.server.uptime %60);
603             var str =  u_d + " day" + (u_d != 1 ? "s, " : ", ") + u_h + " hour" + (u_h != 1 ? "s, " : ", ") + u_m + " minute" + (u_m != 1 ? "s" : "");
604             var ubox = document.createElement('div');
605             ubox.setAttribute("class", "statsbox");
606             ubox.innerHTML = "<span style='font-size: 2rem;'>" + u_f + " days</span><br/><i>since last (re)start.</i><br/><small>" + str;
607             gstats.appendChild(ubox);
608             
609             
610             // Bytes transferred in total
611             var MB = fnmb(json.server.bytes);
612             var KB = (json.server.bytes > 0) ? fnmb(json.server.bytes/json.server.connections) : 0;
613             var KBs = fnmb(json.server.bytes/json.server.uptime);
614             var mbbox = document.createElement('div');
615             mbbox.setAttribute("class", "statsbox");
616             mbbox.innerHTML = "<span style='font-size: 2rem;'>" + MB + "</span><br/><i>transferred in total.</i><br/><small>" + KBs + "/sec, " + KB + "/request";
617             gstats.appendChild(mbbox);
618             
619             // connections in total
620             var cons = fn(json.server.connections);
621             var cps = Math.floor(json.server.connections/json.server.uptime*100)/100;
622             var conbox = document.createElement('div');
623             conbox.setAttribute("class", "statsbox");
624             conbox.innerHTML = "<span style='font-size: 2rem;'>" + cons + " conns</span><br/><i>since server started.</i><br/><small>" + cps + " requests per second";
625             gstats.appendChild(conbox);
626             
627             // threads working
628             var tpc = json.mpm.threadsPerChild;
629             var activeThreads = fn(json.mpm.activeServers * json.mpm.threadsPerChild);
630             var maxThreads = json.mpm.maxServers * json.mpm.threadsPerChild;
631             var tbox = document.createElement('div');
632             tbox.setAttribute("class", "statsbox");
633             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.";
634             gstats.appendChild(tbox);
635         
636         
637         
638         window.setTimeout(waitTwo, updateSpeed*1000);
639         
640         // resize pane
641         document.getElementById('leftpane').style.height = document.getElementById('wrapper').getBoundingClientRect().height + "px";
642         
643         // Do we have extended info and module lists??
644         if (json.server.extended) document.getElementById('threads_button').style.display = 'block';
645         if (json.server.modules && json.server.modules.length > 0) {
646             var panel = document.getElementById('modules_breakdown');
647             var list = "<ul>";
648             for (var i in json.server.modules) {
649                 var mod = json.server.modules[i];
650                 list += "<li>" + mod + "</li>";
651             }
652             list += "</ul>";
653             panel.innerHTML = list;
654             
655             document.getElementById('modules_button').style.display = 'block';
656         }
657        
658         
659     } else if (json === false) {
660         waitTwo();
661     }
662 }
663
664 function refreshThreads(json, state) {
665     var box = document.getElementById('threads_breakdown');
666     box.innerHTML = "";
667     for (var i in json.processes) {
668         var proc = json.processes[i];
669         var phtml = '<div style="color: #DDF">';
670         if (!proc.active) phtml = '<div title="this process is inactive" style="color: #999;">';
671         phtml += "<h3>Process " + i + ":</h3>";
672         phtml += "<b>PID:</b> " + (proc.pid||"None (not active)") + "<br/>";
673         if (proc.threads && proc.active) {
674             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>";
675             for (var j in proc.threads) {
676                 var thread = proc.threads[j];
677                 thread.request = (thread.request||"(Unknown)").replace(/[<>]+/g, "");
678                 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>";
679             }
680             phtml += "</table>";
681         } else {
682             phtml += "<p>No thread information avaialable</p>";
683         }
684         phtml += "</div>";
685         box.innerHTML += phtml;
686     }
687 }
688
689 function waitTwo() {
690     getAsync(location.href + "?view=json&rnd=" + Math.random(), null, refreshCharts)
691 }
692
693     function showPanel(what) {
694         var items = ['dashboard','misc','threads','modules'];
695         for (var i in items) {
696             var item = items[i];
697             var btn = document.getElementById(item+'_button');
698             var panel = document.getElementById(item+'_panel');
699             if (item == what) {
700                 btn.setAttribute("class", "btn active");
701                 panel.style.display = 'block';
702             } else {
703                 btn.setAttribute("class", "btn");
704                 panel.style.display = 'none';
705             }
706         }
707         
708         // special constructors
709         if (what == 'threads') {
710             getAsync(location.href + "?view=json&extended=true&rnd=" + Math.random(), null, refreshThreads)
711         }
712     }
713     
714     function fn(num) {
715         num = num + "";
716         num = num.replace(/(\d)(\d{9})$/, '$1,$2');
717         num = num.replace(/(\d)(\d{6})$/, '$1,$2');
718         num = num.replace(/(\d)(\d{3})$/, '$1,$2');
719         return num;
720     }
721
722     function fnmb(num) {
723         var add = "bytes";
724         var dec = "";
725         var mul = 1;
726         if (num > 1024) { add = "KB"; mul= 1024; }
727         if (num > (1024*1024)) { add = "MB"; mul= 1024*1024; }
728         if (num > (1024*1024*1024)) { add = "GB"; mul= 1024*1024*1024; }
729         if (num > (1024*1024*1024*1024)) { add = "TB"; mul= 1024*1024*1024*1024; }
730         num = num / mul;
731         if (add != "bytes") {
732             dec = "." + Math.floor( (num - Math.floor(num)) * 100 );
733         }
734         return ( fn(Math.floor(num)) + dec + " " + add );
735     }
736
737     function sort(a,b){
738         last_col = -1;
739         var sort_reverse = false;
740         var sortWay = a.getAttribute("sort_" + b);
741         if (sortWay && sortWay == "forward") {
742             a.setAttribute("sort_" + b, "reverse");
743             sort_reverse = true;
744         }
745         else {
746             a.setAttribute("sort_" + b, "forward");
747         }
748         var c,d,e,f,g,h,i;
749         c=a.rows.length;
750         if(c<1){ return; }
751         d=a.rows[1].cells.length;
752         e=1;
753         var j=new Array(c);
754         f=0;
755         for(h=e;h<c;h++){
756             var k=new Array(d);
757             for(i=0;i<d;i++){
758                 cell_text="";
759                 cell_text=a.rows[h].cells[i].textContent;
760                 if(cell_text===undefined){cell_text=a.rows[h].cells[i].innerText;}
761                 k[i]=cell_text;
762             }
763             j[f++]=k;
764         }
765         var l=false;
766         var m,n;
767         if(b!=lastcol) lastseq="A";
768         else{
769             if(lastseq=="A") lastseq="D";
770             lastseq="A";
771         }
772
773         g=c-1;
774
775         for(h=0;h<g;h++){
776             l=false;
777             for(i=0;i<g-1;i++){
778                 m=j[i];
779                 n=j[i+1];
780                 if(lastseq=="A"){
781                     var gt = (m[b]>n[b]) ? true : false;
782                     var lt = (m[b]<n[b]) ? true : false;
783                     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; }
784                     if (sort_reverse) {gt = (!gt); lt = (!lt);}
785                     if(gt){
786                         j[i+1]=m;
787                         j[i]=n;
788                         l=true;
789                     }
790                 }
791                 else{
792                     if(lt){
793                         j[i+1]=m;
794                         j[i]=n;
795                         l=true;
796                     }
797                 }
798             }
799             if(l===false){
800                 break;
801             }
802         }
803         f=e;
804         for(h=0;h<g;h++){
805             m=j[h];
806             for(i=0;i<d;i++){
807                 if(a.rows[f].cells[i].innerText!==undefined){
808                     a.rows[f].cells[i].innerText=m[i];
809                 }
810                 else{
811                     a.rows[f].cells[i].textContent=m[i];
812                 }
813             }
814             f++;
815         }
816         lastcol=b;
817     }
818
819     
820     var CPUmax =            1000000;
821     
822     
823     var showing = false;
824     function showDetails() {
825         for (i=1; i < 1000; i++) {
826             var obj = document.getElementById("srv_" + i);
827             if (obj) {
828                 if (showing) { obj.style.display = "none"; }
829                 else { obj.style.display = "block"; }
830             }
831         }
832         var link = document.getElementById("show_link");
833         showing = (!showing);
834         if (showing) { link.innerHTML = "Hide thread information"; }
835         else { link.innerHTML = "Show thread information"; }
836     }
837
838     var showing_modules = false;
839     function show_modules() {
840
841         var obj = document.getElementById("modules");
842         if (obj) {
843             if (showing_modules) { obj.style.display = "none"; }
844             else { obj.style.display = "block"; }
845         }
846         var link = document.getElementById("show_modules_link");
847         showing_modules = (!showing_modules);
848         if (showing_modules) { link.innerHTML = "Hide loaded modules"; }
849         else { link.innerHTML = "Show loaded modules"; }
850     }
851 ]==]
852
853 quokka_js = [==[
854 /*
855  * 
856  * Licensed under the Apache License, Version 2.0 (the "License");
857  * you may not use this file except in compliance with the License.
858  * You may obtain a copy of the License at
859  * 
860  *     http://www.apache.org/licenses/LICENSE-2.0
861  * 
862  * Unless required by applicable law or agreed to in writing, software
863  * distributed under the License is distributed on an "AS IS" BASIS,
864  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
865  * See the License for the specific language governing permissions and
866  * limitations under the License.
867  */
868
869 // Traffic shaper
870 function quokka_fnmb(num) {
871     var add = "b";
872     var dec = "";
873     var mul = 1;
874     if (num > 1024) { add = "KB"; mul= 1024; }
875     if (num > (1024*1024)) { add = "MB"; mul= 1024*1024; }
876     if (num > (1024*1024*1024)) { add = "GB"; mul= 1024*1024*1024; }
877     if (num > (1024*1024*1024*1024)) { add = "TB"; mul= 1024*1024*1024*1024; }
878     num = num / mul;
879     if (add != "b" && num < 10) {
880         dec = "." + Math.floor( (num - Math.floor(num)) * 100 );
881     }
882     return ( Math.floor(num) + dec + " " + add );
883 }
884
885 // Hue, Saturation and Lightness to Red, Green and Blue:
886 function quokka_internal_hsl2rgb (h,s,l)
887 {
888     var min, sv, switcher, fract, vsf;
889     h = h % 1;
890     if (s > 1) s = 1;
891     if (l > 1) l = 1;
892     var v = (l <= 0.5) ? (l * (1 + s)) : (l + s - l * s);
893     if (v === 0)
894         return { r: 0, g: 0, b: 0 };
895
896     min = 2 * l - v;
897     sv = (v - min) / v;
898     var sh = (6 * h) % 6;
899     switcher = Math.floor(sh);
900     fract = sh - switcher;
901     vsf = v * sv * fract;
902
903     switch (switcher)
904     {
905         case 0: return { r: v, g: min + vsf, b: min };
906         case 1: return { r: v - vsf, g: v, b: min };
907         case 2: return { r: min, g: v, b: min + vsf };
908         case 3: return { r: min, g: v - vsf, b: v };
909         case 4: return { r: min + vsf, g: min, b: v };
910         case 5: return { r: v, g: min, b: v - vsf };
911     }
912     return {r:0, g:0, b: 0};
913 }
914
915 // RGB to Hex conversion
916 function quokka_internal_rgb2hex(r, g, b) {
917     return "#" + ((1 << 24) + (Math.floor(r) << 16) + (Math.floor(g) << 8) + Math.floor(b)).toString(16).slice(1);
918 }
919
920
921 // Generate color list used for charts
922 var colors = [];
923 var rgbs = []
924 var numColorRows = 6;
925 var numColorColumns = 20;
926 for (var x=0;x<numColorRows;x++) {
927     for (var y=0;y<numColorColumns;y++) {
928         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]
929         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));
930         
931         // Light (primary) color:
932         var hex = quokka_internal_rgb2hex(color.r*255, color.g*255, color.b*255);
933         
934         // Darker variant for gradients:
935         var dhex = quokka_internal_rgb2hex(color.r*131, color.g*131, color.b*131);
936         
937         // Medium variant for legends:
938         var mhex = quokka_internal_rgb2hex(color.r*200, color.g*200, color.b*200);
939         
940         colors.push([hex, dhex, color, mhex]);
941     }
942 }
943
944
945 /* Function for drawing pie diagrams
946  * Example usage:
947  * quokkaCircle("canvasName", [ { title: 'ups', value: 30}, { title: 'downs', value: 70} ] );
948  */
949
950 function quokkaCircle(id, tags, opts) {
951     // Get Canvas object and context
952     var canvas = document.getElementById(id);
953     var ctx=canvas.getContext("2d");
954     
955     // Calculate the total value of the pie
956     var total = 0;
957     var k;
958     for (k in tags) {
959         tags[k].value = Math.abs(tags[k].value);
960         total += tags[k].value;
961     }
962     
963     
964     
965     // Draw the empty pie
966     var begin = 0;
967     var stop = 0;
968     var radius = (canvas.height*0.75)/2;
969     ctx.clearRect(0, 0, canvas.width, canvas.height);
970     ctx.beginPath();
971     ctx.shadowBlur = 6;
972     ctx.shadowOffsetX = 6;
973     ctx.shadowOffsetY = 6;
974     ctx.shadowColor = "#555";
975     ctx.lineWidth = (opts && opts.hires) ? 6 : 2;
976     ctx.strokeStyle = "#222";
977     ctx.arc((canvas.width-140)/2,canvas.height/2,radius, 0, Math.PI * 2);
978     ctx.closePath();
979     ctx.stroke();
980     ctx.fill();
981     ctx.shadowBlur = 0;
982     ctx.shadowOffsetY = 0;
983     ctx.shadowOffsetX = 0;
984     
985     
986     // Draw a title if set:
987     if (opts && opts.title) {
988         ctx.font= (opts && opts.hires) ? "28px Sans-Serif" : "15px Sans-Serif";
989         ctx.fillStyle = "#000000";
990         ctx.textAlign = "center";
991         ctx.fillText(opts.title,(canvas.width-140)/2, (opts && opts.hires) ? 30:15);
992         ctx.textAlign = "left";
993     }
994     
995     ctx.beginPath();
996     var posY = 50;
997     var left = 120 + ((canvas.width-140)/2) + ((opts && opts.hires) ? 40 : 25)
998     for (k in tags) {
999         var val = tags[k].value;
1000         stop = stop + (2 * Math.PI * (val / total));
1001         
1002         // Make a pizza slice
1003         ctx.beginPath();
1004         ctx.lineCap = 'round';
1005         ctx.arc((canvas.width-140)/2,canvas.height/2,radius,begin,stop);
1006         ctx.lineTo((canvas.width-140)/2,canvas.height/2);
1007         ctx.closePath();
1008         ctx.lineWidth = 0;
1009         ctx.stroke();
1010         
1011         // Add color gradient
1012         var grd=ctx.createLinearGradient(0,canvas.height*0.2,0,canvas.height);
1013         grd.addColorStop(0,colors[k % colors.length][1]);
1014         grd.addColorStop(1,colors[k % colors.length][0]);
1015         ctx.fillStyle = grd;
1016         ctx.fill();
1017         begin = stop;
1018         
1019         // Make color legend
1020         ctx.fillRect(left, posY-((opts && opts.hires) ? 15 : 10), (opts && opts.hires) ? 14 : 7, (opts && opts.hires) ? 14 : 7);
1021         
1022         // Add legend text
1023         ctx.shadowColor = "rgba(0,0,0,0)"
1024         ctx.font= (opts && opts.hires) ? "22px Sans-Serif" : "12px Sans-Serif";
1025         ctx.fillStyle = "#000";
1026         ctx.fillText(tags[k].title + " (" + Math.floor(val) + (opts && opts.pct ? "%" : "") + ")",left+20,posY);
1027         
1028         posY += (opts && opts.hires) ? 28 : 14;
1029     }
1030     
1031 }
1032
1033
1034 /* Function for drawing line charts
1035  * Example usage:
1036  * 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" } );
1037  */
1038 function quokkaLines(id, titles, values, options, sums) {
1039     var canvas = document.getElementById(id);
1040     var ctx=canvas.getContext("2d");
1041     // clear the canvas first
1042     ctx.clearRect(0, 0, canvas.width, canvas.height);
1043     
1044     
1045
1046
1047     ctx.lineWidth = 0.25;
1048     ctx.strokeStyle = "#000000";
1049     
1050     var lwidth = 300;
1051     var lheight = 75;
1052     wspace = (options && options.hires) ? 110 : 55;
1053     var rectwidth = canvas.width - lwidth - wspace;
1054     var stack = options ? options.stack : false;
1055     var curve = options ? options.curve : false;
1056     var title = options ? options.title : null;
1057     var spots = options ? options.points : false;
1058     var noX = options ? options.nox : false;
1059     var verts = options ? options.verts : true;
1060     if (noX) {
1061         lheight = 0;
1062     }
1063     
1064     
1065     // calc rectwidth if titles are large
1066     var nlwidth = 0
1067     for (var k in titles) {
1068         ctx.font= (options && options.hires) ? "24px Sans-Serif" : "12px Sans-Serif";
1069         ctx.fillStyle = "#00000";
1070         var x = parseInt(k)
1071         if (!noX) {
1072             x = x + 1;
1073         }
1074         var sum = 0
1075         for (var y in values) {
1076             sum += values[y][x]
1077         }
1078         var t = titles[k] + (!options.nosum ? " (" + ((sums && sums[k]) ? sums[k] : sum.toFixed(0)) + ")" : "");
1079         var w = ctx.measureText(t).width + 48;
1080         if (w > lwidth && w > nlwidth) {
1081             nlwidth = w
1082         }
1083         if (nlwidth > 0) {
1084             rectwidth -= nlwidth - lwidth
1085             lwidth = nlwidth
1086         }
1087     }
1088     
1089     // Draw a border
1090     ctx.lineWidth = 0.5;
1091     ctx.strokeRect((wspace*0.75), 30, rectwidth, canvas.height - lheight - 40);
1092     
1093     // Draw a title if set:
1094     if (title != null) {
1095         ctx.font= (options && options.hires) ? "24px Sans-Serif" : "15px Sans-Serif";
1096         ctx.fillStyle = "#00000";
1097         ctx.textAlign = "center";
1098         ctx.fillText(title,rectwidth/2, 20);
1099     }
1100     
1101     // Draw legend
1102     ctx.textAlign = "left";
1103     var posY = 50;
1104     for (var k in titles) {
1105         var x = parseInt(k)
1106         if (!noX) {
1107             x = x + 1;
1108         }
1109         var sum = 0
1110         for (var y in values) {
1111             sum += values[y][x]
1112         }
1113         
1114         var title = titles[k] + (!options.nosum ? (" (" + ((sums && sums[k]) ? sums[k] : sum.toFixed(0)) + ")") : "");
1115         if (options && options.lastsum) {
1116             title = titles[k] + " (" + values[values.length-1][x].toFixed(0) + ")";
1117         }
1118         ctx.fillStyle = colors[k % colors.length][3];
1119         ctx.fillRect(wspace + rectwidth + 75 , posY-((options && options.hires) ? 18:9), (options && options.hires) ? 20:10, (options && options.hires) ?20:10);
1120         
1121         // Add legend text
1122         ctx.font= (options && options.hires) ? "24px Sans-Serif" : "14px Sans-Serif";
1123         ctx.fillStyle = "#00000";
1124         ctx.fillText(title,canvas.width - lwidth + ((options && options.hires) ? 100:60), posY);
1125         
1126         posY += (options && options.hires) ? 30:15;
1127     }
1128     
1129     // Find max and min
1130     var max = null;
1131     var min = 0;
1132     var stacked = null;
1133     for (x in values) {
1134         var s = 0;
1135         for (y in values[x]) {
1136             if (y > 0 || noX) {
1137                 s += values[x][y];
1138                 if (max === null || max < values[x][y]) {
1139                     max = values[x][y];
1140                 }
1141                 if (min === null || min > values[x][y]) {
1142                     min = values[x][y];
1143                 }
1144             }
1145         }
1146         if (stacked === null || stacked < s) {
1147             stacked = s;
1148         }
1149     }
1150     if (min == max) max++;
1151     if (stack) {
1152         min = 0;
1153         max = stacked;
1154     }
1155     
1156     
1157     // Set number of lines to draw and each step
1158     var numLines = 5;
1159     var step = (max-min) / (numLines+1);
1160     
1161     // Prettify the max value so steps aren't ugly numbers
1162     if (step %1 != 0) {
1163         step = (Math.round(step+0.5));
1164         max = step * (numLines+1);
1165     }
1166     
1167     // Draw horizontal lines
1168     
1169     for (x = -1; x <= numLines; x++) {
1170         ctx.beginPath();
1171         var y = 30 + (((canvas.height-40-lheight) / (numLines+1)) * (x+1));
1172         ctx.moveTo(wspace*0.75, y);
1173         ctx.lineTo(wspace*0.75 + rectwidth, y);
1174         ctx.lineWidth = 0.25;
1175         ctx.stroke();
1176         
1177         // Add values
1178         ctx.font= (options && options.hires) ? "20px Sans-Serif" : "12px Sans-Serif";
1179         ctx.fillStyle = "#000000";
1180         
1181         var val = Math.round( ((max-min) - (step*(x+1))) );
1182         if (options && options.traffic) {
1183             val = quokka_fnmb(val);
1184         }
1185         ctx.textAlign = "left";
1186         ctx.fillText( val,canvas.width - lwidth - 20, y+8);
1187         ctx.textAlign = "right";
1188         ctx.fillText( val,wspace-32, y+8);
1189         ctx.closePath();
1190     }
1191     
1192     
1193     
1194     // Draw vertical lines
1195     var sx = 1
1196     var numLines = values.length-1;
1197     var step = (canvas.width - lwidth - wspace*0.75) / values.length;
1198     while (step < 24) {
1199         step *= 2
1200         sx *= 2
1201     }
1202     
1203     
1204     if (verts) {
1205         ctx.beginPath();
1206         for (var x = 1; x < values.length; x++) {
1207             if (x % sx == 0) {
1208                 var y = (wspace*0.75) + (step * (x/sx));
1209                 ctx.moveTo(y, 30);
1210                 ctx.lineTo(y, canvas.height - 10 - lheight);
1211                 ctx.lineWidth = 0.25;
1212                 ctx.stroke();
1213             }
1214         }
1215         ctx.closePath();
1216     }
1217     
1218     
1219     
1220     // Some pre-calculations of steps
1221     var step = (rectwidth) / (values.length > 1 ? values.length-1:1);
1222     
1223     // Draw X values if noX isn't set:
1224     if (noX != true) {
1225         ctx.beginPath();
1226         for (var i = 0; i < values.length; i++) {
1227             zz = 1
1228             var x = (wspace*0.75) + ((step) * i);
1229             var y = canvas.height - lheight + 5;
1230             if (i % sx == 0) {
1231                 ctx.translate(x, y);
1232                 ctx.moveTo(0,0);
1233                 ctx.lineTo(0,-15);
1234                 ctx.stroke();
1235                 ctx.rotate(45*Math.PI/180);
1236                 ctx.textAlign = "left";
1237                 var val = values[i][0];
1238                 if (val.constructor.toString().match("Date()")) {
1239                     val = val.toDateString();
1240                 }
1241                 ctx.fillText(val.toString(), 0, 0);
1242                 ctx.rotate(-45*Math.PI/180);
1243                 ctx.translate(-x,-y);
1244             }
1245         }
1246         ctx.closePath();
1247         
1248     }
1249     
1250     
1251     
1252     
1253     // Draw each line
1254     var stacks = [];
1255     var pstacks = [];
1256     for (k in values) { if (k > 0) { stacks[k] = 0; pstacks[k] = canvas.height - 40 - lheight; }}
1257     
1258     for (k in titles) {
1259         var maxY = 0, minY = 99999;
1260         ctx.beginPath();
1261         var color = colors[k % colors.length][0];
1262         var f = parseInt(k) + 1;
1263         if (noX) {
1264             f = parseInt(k);
1265         }
1266         var value = values[0][f];
1267         var step = rectwidth / numLines;
1268         var x = (wspace*0.75);
1269         var y = (canvas.height - 10 - lheight) - (((value-min) / (max-min)) * (canvas.height - 40 - lheight));
1270         var py = y;
1271         if (stack) {
1272             stacks[0] = stacks[0] ? stacks[0] : 0
1273             y -= stacks[0];
1274             pstacks[0] = stacks[0];
1275             stacks[0] += (((value-min) / (max-min)) * (canvas.height - 40 - lheight));
1276         }
1277         
1278         // Draw line
1279         ctx.moveTo(x, y);
1280         var pvalY = y;
1281         var pvalX = x;
1282         for (var i in values) {
1283             if (i > 0) {
1284                 x = (wspace*0.75) + (step*i);
1285                 var f = parseInt(k) + 1;
1286                 if (noX == true) {
1287                     f = parseInt(k);
1288                 }
1289                 value = values[i][f];
1290                 y = (canvas.height - 10 - lheight) - (((value-min) / (max-min)) * (canvas.height - 40 - lheight));
1291                 if (stack) {
1292                     y -= stacks[i];
1293                     pstacks[i] = stacks[i];
1294                     stacks[i] += (((value-min) / (max-min)) * (canvas.height - 40- lheight));
1295                 }
1296                 if (y > maxY) maxY = y;
1297                 if (y < minY) minY = y;
1298                 // Draw curved lines??
1299                 /* We'll do: (x1,y1)-----(x1.5,y1)
1300                  *                          |
1301                  *                       (x1.5,y2)-----(x2,y2)
1302                  * with a quadratic beizer thingy
1303                 */
1304                 if (curve) {
1305                     ctx.bezierCurveTo((pvalX + x) / 2, pvalY, (pvalX + x) / 2, y, x, y);
1306                     pvalX = x;
1307                     pvalY = y;
1308                 }
1309                 // Nope, just draw straight lines
1310                 else {
1311                     ctx.lineTo(x, y);
1312                 }
1313                 if (spots) {
1314                     ctx.fillStyle = color;
1315                     ctx.translate(x-2, y-2);
1316                     ctx.rotate(-45*Math.PI/180);
1317                     ctx.fillRect(-2,1,4,4);
1318                     ctx.rotate(45*Math.PI/180);
1319                     ctx.translate(-x+2, -y+2);
1320                 }
1321             }
1322         }
1323         
1324         ctx.lineWidth = 4;
1325         ctx.strokeStyle = color;
1326         ctx.stroke();
1327         
1328         
1329         if (minY == maxY) maxY++;
1330         
1331         // Draw stack area
1332         if (stack) {
1333             ctx.globalAlpha = 0.65;
1334             for (i in values) {
1335                 if (i > 0) {
1336                     var f = parseInt(k) + 1;
1337                     if (noX == true) {
1338                         f = parseInt(k);
1339                     }
1340                     x = (wspace*0.75) + (step*i);
1341                     value = values[i][f];
1342                     y = (canvas.height - 10 - lheight) - (((value-min) / (max-min)) * (canvas.height - 40 - lheight));
1343                     y -= stacks[i];
1344                 }
1345             }
1346             var pvalY = y;
1347             var pvalX = x;
1348             if (y > maxY) maxY = y;
1349             if (y < minY) minY = y;
1350             for (i in values) {
1351                 var l = values.length - i - 1;
1352                 x = (wspace*0.75) + (step*l);
1353                 y = canvas.height - 10 - lheight - pstacks[l];
1354                 if (y > maxY) maxY = y;
1355                 if (y < minY) minY = y;
1356                 if (curve) {
1357                     ctx.bezierCurveTo((pvalX + x) / 2, pvalY, (pvalX + x) / 2, y, x, y);
1358                     pvalX = x;
1359                     pvalY = y;
1360                 }
1361                 else {
1362                     ctx.lineTo(x, y);
1363                 }
1364             }
1365             ctx.lineTo((wspace*0.75), py - pstacks[0]);
1366             ctx.lineWidth = 0;
1367             var grad = ctx.createLinearGradient(0, minY, 0, maxY);
1368             grad.addColorStop(0.25, colors[k % colors.length][0])
1369             grad.addColorStop(1, colors[k % colors.length][1])
1370             ctx.strokeStyle = colors[k % colors.length][0];
1371             ctx.fillStyle = grad;
1372             ctx.fill();
1373             ctx.fillStyle = "#000"
1374             ctx.strokeStyle = "#000"
1375             ctx.globalAlpha = 1;
1376         }
1377         ctx.closePath();
1378     }
1379     
1380     // draw feather
1381     base_image = new Image();
1382     base_image.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAAEACAYAAAB7+X6nAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACJQAAAiUBweyXgQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic7Z13vBXF2ce/z+65hUuxYUNEuFyaF0EEwRaDPRobKsYSW0w0auwKWBKPFUEs0ddujJpEDWo0aiyxYBcEgqKXJsJFEQuitFvP2XneP07b0+utnN/nA/fs7MyzszO/eZ5nnpndFYrocNC7q7s1SMNvUY40qsNQa3NVvkGZbYw+02Ndl6fEW9OciSxp6coWUVg03Fd5hqreoipboaAKqAT/Bv6pUmtUzt78kqX/TSevSIAOgrX39Nmi1PI8BhwORHe4ESJkCP9Vo/xlM2m6QC5Z2ZBMbpEAHQAbHqiqttU8h1IVTozq8BgCmMh5VX3H+Dlyy0nL1iWSXSRAO0f9fVV7gvkPsEXUibgRL7EawEUGmec4Zr9EJLBa/haKyBUN9/ffD8yrxHY+BIauxCcRmyyA6AjLtp7TO6vKYsUUCdBOUX9f5TGq+jLQPVkeCf4nCciQAGPXNzI5oYwi2hc23tv/dEv0IcBOmTGdGTBucyCoomI4dLNJS18NiShqgHaGjfdWnizwF9J1fhrEaYfAUBcVHvj2lmFdQ/mKBGhHqLuv6ghB/kqm/eLq3IT2Pwoaytun3DScG0otEqCdYMO9VWNRnQ6UQHBqlwHE/SN6tCeH6oV6/8gSKBKgXaDu7srdbfR5oDw/SRr5mUo7CDv8tOang6BIgDZH4wOVA7HkRU3h7adEgtGeoMND08FwolgyHsCT00WLKAjqH6zq7fj1TWCbvARJ5I9ClCJInl+OUA34iEW0AfTu6m71dsO7qOyaKp9Iut5Eg9M+SRcWJmaqaDlmUNEEtAHUi1VvNf49XednAAM0EWUIogkTmgYmChb5LWt0kQBtgLptK6cBR+UpRoF1QHki7z+pahd1O4cjiwRoZdTdV3WmIBfnK0dgAa41grDjl+lUMJC3d17RpiKyw8b7+h0kKk+Q1exLEnhqMhMYQjBmkKxcXIqGJQKgsLZIgFbChrsHDLEsXgEqsi0bQ4D/AdsCPaMzEWP+JcEvV0rAEviLJqAV8NXF1Vs2fCc3omyep6gvAr4+fRKedTt8ZBQW7lkkQAtDx2OXlvr+bpoY529gXk4yAmHhDcBHKCOT5YvfB0BSfyCYZIoEaGF8XzngJoVDARpX2yZHMUZV7wCOy7xITPwgRjsE4RQJ0IL4dsKgIxW5PHSshpH+OvkoWzmCTBVLTiWl00e2u4RAij5Ai+H7y4cMQPQxYtq+cY10RclGEzxv0EqUnTLJnGQfQLK8RQK0BL66uHcXx3L+CWwWe06NVDdvsD7IUNRSFZkpcHymy8MxVwv8SU6GjcXFoBZAaVnFvaqMSHa+6UfZobQHPlKr9AYR/RPKw4WqlwAqkRmjwrdFDVBgfDNx4HmqnJYmW7+mn6z3U+ZQuUBVLiHbPQJZ7BISdFWRAAXENxMGjsZwa2gFLhWa18lglPqEJ1X/ArodMCo6ubC7hASWFQlQIHx1cfWWOPJPVMrCT+WEiJCYDNs1rrFnJkhfatk8gvDHwtQsyS4hARUWFwlQAKgXy8b/hCp9CTyJE/6nMU/quOGrY7ga1rqSmsSyfm2M9X9Aac4VSrVLSDHGx0qnQf6njnxWdAILgFU/DbpchIPB1ccanIoFvS4NnolSz8pWjT9Yb3TZxhwAICJXqjFHAMPzrZOq1pkm62vjkzW+Rmk2zZSpw1ao9AF6A1tt+1P3T4sEyBOrLhg8UpXrFA2stiXYmiGQlAj+ehmjDt+JzceIvI/qe6mupyqBXUKKUSOrjY/VpknWOc3i8zdLifrpoYbtCSwWDUwuiP/JA3N9RQLkgdUTBnVvbuBJoJTgKg1uIgT/JiSCashT79bwnfV2t16+PxjDSwoGP98ZR9aroc7x0ag+mo0fNX6rBIduGLbUwIpg6F/WUJgJxU2heaGpjruBKgTX6I8QgVBSIiKE1uRV8TXx8PoVpaersiMB259zx2YOKRIgH6z8/aDjUTkldBzu3PDoT6DyQ0QI/hVAVR7H0oVqeALJw/HLDurxBOIQxV3BOaD2t4P62R7rY6AHouH4uzvyEk4LnwsSITo6s9o2zbuY0pLHgf1jzrUkZm83ZcloKD4YkjXUO9YjlvUPlB6RbdYSfhoX11O5UVNAE5wWmqi0c9Qu/SVG9o87l3Y3eO4QlRdCv4smIEt8tfLba0Vkz7AaT2D7wwM4pe3nSUv97xiPvTC0QcftF0TJLrRGMPw79LNoArLAl2cO2VfhTZHgo9spVX6QErFqPRCF+8H2+apNecktoKfGnHPldz3KVTgi1Gw3ZcnQ0EFRA2SIFSftsgXG+RuCHRn9kQ6KHuWBBA33pkb1naLnaknJEFU9JaoMLgdRcbFBw4985esnKPKY+7hIgAyhHv9DwShaJC2kuENE0IDzD9FEQII+QsBhfLrBcp6vwPMJRiQmJoC7WEA2uPdxJYwoZg7HUedxd0KRABlgxak7/1rhmLCDFmX7XUQIdZuLCODuI/mh2Wf/obycKxQGRc6F/YIASaJkE/6RNrScHi/2nrp0pTuhSIA0+PKEQb0clT8HuyicHj/vj6h8NxHc837gDyW200MME4MZo2ICRJXOMLQcqz1SEEGE/4tNK04D08DxlDyIYcvA8m5gyodrmTdutc8QnBZKJC3Qc//a8f6F0xHuVZXyqDLBpeOoqWSS6WX8NRNMLxNOIfXzbW5e8kZsalEDpMCyk4aeqY4eFvtoVqa2XyVIAtF14jfnf332kFNQDkjgF7hkB+XkEVpONIVUlTslATWKBEiCz08e1hu/My3c0GEbTEa2P5oz8scusKHBYUpI50ap/BS2P9vQcpgIrjqJxXoP8mii+yyagARQEPGZh1Rlc1yqPCq6F2UGQipfoiOBgc0hc/tsWHBPo1hXAduF1Xisyk6k8mOvqUQiirGmJ86MROpjVB7ZeuriDYnutRgISoBlx1afp5YEHCZ3IAZwv2cneQAomA+MKntZJfZqcBYglCUNDrnKh66TSHZ0XYCgvLj4QCSvT7EG9Lp14YpE91rUADFYNH5YP4PcHLWfL2oUSnAUxqYTM/oF1Lq7398WzBI101DKwg6ikfjRG6dZXBrBJTu5ExivPYJ5H03W+VDUAFFQL9ayT3aZocK+uEaqJBiFMSM9ZvQD8C0lJUPE+EYg+mZCbREsn2j0Jh79SfImkhk4dhxjDdnxzoWfJ7vnogZwYen/drnIKPtqjA2NtsHEjNrEtl/g0r51Azag3B7vF7j/ScbTy4RaIcUU0ihPpOp8KBIgjIXjhg8EbojqgBARTAoihJd+XURQ3t3pHzVP1JbV/FaV4THnwrLjzUAMEQzJiZDejKhHrSnp7rtoAggo+88P3+U1hODuXBI5U8EfaZxAMFiMwSn73CppXIIE3wHoluM2G7FyotI0gRlIqfLdPfp077sXjU9370UNACw5bNiZqBwQpfKTRt9cTmCiEarc3//Jz+aIp/FPqmyTcJSaaI2Q0AlMFFHMfArpWOr8KZN73+Q1wLIjh27r91kLVQJv3IqM/tROYJRGIJz+k1PaPNAypZvZUINometcQs0SNdLTTi8zm0KqcF+fexeek8n9b/IawNcsd6qyRba2X+Nsv6CqVw96YskPluEOVcqS2f5oJzBz25/hFLLOOFyX6f1v0hpg4cHDDxf0hWQ2GGJHv8anRf5+WlUyeMQXLPi5qLyRUE6M7U8+vYy3/YmnkPHaQy25bqf7F1yTaRtsshrgk4OHdRXlzsjolTS2n8QbO0P/0MuofkoxckfCMG0C258wtBxl++Onl6m0h6h+X+FzpmXTDpvsYlCpXyar0A8hauUNIOGCS7CcuP4Prbyp8sqgFz7771IZdiroLgQe3QosDCnuDT1BrohrYUkj6UQGd7g8rtc6hssHMsZuSjFY12/9cE3CmH8ybJImoGb/4aMt+ACwUzteyVW+K81xjI4wXc3i0iZrIRaV0bJinMmkJsZlHpKo/DRTyJrVJV1GjHpgri+bttjkTMCMsWM9YuR+NWIXYuVNDH8d8tKnn5Y22ucClfEqO6LKU5mYhCo/lRmJrreqI+dn2/mwCWqA+fsMv8Rjy61A4kBK6G8iRy04ml1pdeq3BzqbN68vabC+QNgmxapc6uml67oSrFPi8sSMfgWRv/V9rObUrBoiiE1KA8wbMmaAs8bzezU057fyFk6bNvi1eatK6q0JqGzjPpdwNTHV9DJquhejEZJoj6ATuB5jJubaJpuUBvjfoFH/QTjM09W8a1eYnwE5234RvtPyxgG2r6Lc4HyB0D2jhzoKZPtD5dXi/Mq/18Rt9swUm4wGmDdo1FEoh6Hg32hXYmiOC6RkYfvV0asGP794g1FzDUr3qBBtFrafbG1/dJ0/XbFy6/vyaZdNQgN80HvPLmVdfJ8BlaGRY3c179gVum/mQZeotfgF3zpbDN++9McdgUUIpfna/iSh5VTaw6/oXv3/WTM7m7aIxSahAcrKnYkYqXSPUqfe7o+hKdGCS9xsIOQnRHbdXLLfW2/5Ra0bUSmN8gtytP2JQsvJwseqYGBKvp0Pm4AGmFM5sg9YC4GKWBttdeGdkm7OvuG0sFftzke07UfeHPL2vAMWHTB0mGLPQ7ASzhRcMt2ysggtJ9Ueqiy0u2/crd8jtY05NksYm0AkUP6MBr7SoUqkcxBMA4O1gkaR4Ns4lfine4IIljXG5hIANfYUJKBBw3KVcG9qWCBR28lDUgP9GqhMomcDwzIUJPohUZ+onlaIzodObgJm9xt1ECpHRyW61a1hG3+dPSvTlTdR/jl0xrxPFu434ueq8otMNnbGOZMZTC9TTSFVrYv6/yt/1R9CpyVATXV1qSB3Jc0QbFxTL0Mw1CfzwEO2H8Xx++3rANTRaxP4BcmJkGhWkXBzRxrbjz424F/z7ylkO3VaAjQ0VFyiwSdwU0GVbXz19uw4JzB+182jwz6cu6hm32H7q/LzNDtyooiQTWg5afgYPqSx8axCt1OnJMAHvffcQVWugkgbp4JTJ0NVpS7ugc5Ix/gcy3NjILN1TWynZf1AZ6plZU1EBPm4udEcPuDlpU0FbSg6KQFKPM4toN3caamIIMJWzkZrjtv2R+/ZkweHvTd72YI9RhyoKvtmYvs1B9ufZAo5w24sGVv9as2PBW8oOiEBZvcbNRrRE5KdT0YEf6PsooaNsdE3VWk0+CcDGMEbZ/tTESFL2x+3KQXu8Xd1ftH/9bnrQvWcM3Jk3FdI8kGnmwYq1rTIjCq58g+dCU+9hS2dBustu4sZG7WBQ/Tu4bPmr/x0zPCD1bB39HQtzZO9ESGuKWDM4+DBcjGbUlYb9JwhL81/JpRv3phdB9iOfS0W04HnMmmLTNCpCDBrp9HjBH4WSYnMn5PBfcY0yTC7jA1qBWL7ImykyZkayGh5w2VCnea6TrjTg3P6QEwhpiYKad4LYFD5u+W3Lx381twfAOaPGLGnipwjPk5E1CNSkvd3h93oNJHAGWPHerqtqJ9P4Ju6KZDaJbQ9+pbd1YwFUJEbdp0394+f7L7bL0R5uUUf6kBfN7ZMqH79k3mfDt1tmPFYR6DmFIRBLjkLh//vfzunvr/s0Gk0QLcV9b9XGJKe0am1guOXEZZjrRVLpcRXejuAGLzhEkF17o4URkY/rtEfE1EMnovVHoq87zRb9/nX4UHk3I+HjjzEEXbERGyKBDWGIs9n0BRZoVMQ4L1Be3fXZt/VEN2tqcmQlAibOY3yttWF16prPvxx/q6jDlNjxsS9JiZEBI2M9HjbjzHIahH9AcNGNdKEQdWRElW6Gb90x5FhCH+LeAhuc+GWK1hiwm/4LBQ6BQFKm5qvQCTu9epxHZIQ8UQwfrbv2lR2ZyDVuQYVV1w+qvA6g3yLQx1Kszrix6FUjXQ1PtkKpSewLcK2hEyC22SE/mow3h8e6a6aRYiwYpd58xJ9YygvdHgCfNB7zx3AuTBVnqyJoHrv4MXvb5g7ePQh6jd+Vd5WQB3K8Ut3VXpi6Engw5DJp2Xi/hMazpp8uzggxBMBQAyPSgJ1lS86PAFKPM5NChWZ9HKGRPhOms0DAKZJrzNNMjpl7lQnQ+Y/yu5HrexlRAQU9VtEveK1UOjQBJjVb/RwVX4dlZgnEUSYOmrV3Pq5fUcfYmB0ukGXGakIB4qiVH7wRBQRQs5iWK4AvD7qszlfpLtELujQBBDVW0ASRzNzI8IPXbpUPADgCFdJ7NksAkvpMkY6PYYIIRmuWYNY+ud0YnNFhyXAzH5jDlbVgyAzNZwJEUCmVNe8tXF23z3GgvlZfM7MA0s5ESGREwhLd1049+V04nJFhyWAqIYfgc6o0dNn+qFrRZf7AFT0qhynkFlcLjpjQidQQUTvFLL63HxW6JCLQTP7jjkKGBObnmyhJ5NMqtxSXfPWxjl99xgDemBGsqLCf1ldLmnGmO3ia+wuvoczKZ4rOhwBFCyBa9PkyZwIgYw/dOtacQ+AEXNV1rJagggGMHrb8Pnz6zIplis6HAFm991jPDC8kI2uhjuqa97aOLPfmGHA4bnLyrxOGdRrnaX+gm7/SoQORYDpjLcVvSY6Ne9GX18qzXcDWKoTY1Zrk8pKjfR1SidLVG8fUfvx2iSnC4YORYC+fb86haSrfUIuWkGEu0bUfrx2dp/dK1GOT5gpAzmFrBPwrc9j355WfAHQYQgwZ+TIEoP+MbNYaMaNXm/bvj8DqCWXITGzooIRIas6IXDhHktnrc9IbJ7oMARw1ti/ASohGzuautEFfXC3pfNWz+w3Zlvg9KQZsyBC3uZB5eGRy2dPTyumQOgQBJjRd2y5IlclOpdHo/t8UnJ7UMhFCl1ynUIWsE6LSxsaL0h/hcKhQxCgXBp/B+yYKk8Ojf7E3ss/WDGzakwPQcMvVcxhClmoOjVg9MTh37XstC8W7Z4AL1UdWoZhQhrnPIwMG10t29wCII6eTYIl3YxVemGI4IjKSbuvmDMv3eUKjXZPgC38P52B0BtAkXSztDBSN7q+uPsXcz77vOrQMlG5KD9ZmWdKkqVZ0ZNG1X5UsJ2+2aBdE2A6421VuTS24/MlgmUCr1Ff4//xJIVeeUzXcsrkylKPyLjRrej0xaJdE6D3Tit/pSJVoeOCEEF5b/cvZ74fWGeRS+JzthoRalV1792XzXoJYO5Oewz5qGr36nRiC412SwAFUZHLAr9Td3w2RMDWKQAf9t3zEESHJs/YYkQwIPc0l5YMG107++N3++yzxUd9x0x2LPN47dK+i9LWv8BotwR4r3KfXyoyIpuOz4AIC0cv++glAEvNpa260BPItERh7Ojls86z6kzJrH6jryq3m79AdJJgXXM8Tzlpq1NgtFsCiOqk0O9sOz4ZEUT0ZgHzUd/dd0U4MJK/hYmgrAS90Gp2RliOMbP7jn7U43G+FrhBYQvg1d2Xzyz4nv9M0C43hLxXuc8+atg7dOz6nHJOx8G0r+wtnSdYDkatSxL1pYbzpkLmm0GA5cBTIroQtQ4wpXI1sHVMxnpVMvq4Q0ugXRLAGLks+smbvDo+9PPWUXPn+mZWjemNn6RPDwfKh8qmQnoiANtCIIYhkiSfyDVjamctT3mpFkS7MwHv77hXf+DwkBrPV/UH035s6NrlLwD4uECRkkzqUoAVv4pUsgRm1C7v0yqrfsnQ7gjgWPaFitjutFyJEH7sUuTe/Wre2vjeoL27I/wuUZlUaIkVP+A7UefktnD83GhXBJhZNaaHipwG8Z3oTsv0OJjms23nPgCryZxpsDbPN5ZQAIexCdHjR62Y+01GF25BtCsCNDhdzlKkR74d7yaPoI/vsXTWSgVLkfMzlZEOeRBBRfS3o5fPfifji7Ug2g0BpjPeRvmDO61AGuAOgA/77nkkUJmt+UiHLImgilw8evnsv2ckvBXQbmYBW/ddfQSwUyIPPudZgPDGPsvf/ziYdlFQI2QkI1laMmQwc3CA8/aonXV/WmGtiHZDAEXOgdQdkPWxkdsB3u23z3BV/Xk2MlLlSX0fobJRaFDllD1WfPRMXIE2RrsgwBs77t8f9EDIrAMyPF7ysxXvvhxMuDBbGZnmSQYXEZYgevwetR99krZQG6Bd+ADi0XMVsZLM4bM+DqbdLmDeqfrZ1oqcmKOMjMokgQKP1HftMnLM8kDnv99/r23SFWpttDkBPui9Zxej1umh4wJN/35saOjyNwD8nAOU5+hA5kqEj0X053vUzjpjG1Y3z+o3evzMnca8ZjkmZQSyLdDmBGjwdDkB2LIQHR8+Vu455Lv/1tVUV5cq8vs8tEi29ahR5AzHtg9RlR1m9h3z8Ia6bl+qynSE/R3HfjrnhmohtLkPYLDOgoI6f82C3gPwff3Wx1uY7bORkeN1GxV5B8MqLM63HPMX4gfX2/t89f6qLJqmVdCmBHi98oCBatgDCur8Pb7vine/ARDVC1rC+UtwXA4c7O7yuDwiT2TcMK2INiWAMdaZod+F0gAqgcDPm33221vR3fNZQcy1HgnSfFaz+VeGzdKqaDMfYMbYsR5Fwu/3KZAP8Pb+y2d8AiAWF+YiI8frppShIv8d8/VHa9K1SVugzQjQtLzsEKBXQTtAuQtgRtXY3qqMa0XnL52Mf2bYLK2ONiOAIqcXuAO+7tFz/fMAxmedTfBBz9bUAElIuVFLpU32/GeCNiHAK70P2VJEj4DCdQDKvaPmzvXNGTmyBOE3Be3EDI+T5Hlyn8Xvb8ikXdoCbeIEmhLrWFEtg4I5YU1qyUMAa9dsfizQK1dHLpcyqeqO8JdM26Ut0DYmQCN78gox8gzWkwcuf+O74MnzcpHRQhpg0T617xf8/b6FRKtrgP/0PWw70J8XcuRZlrkb4M0++1U7IvvkIiOXMmllKA9m3jJtg7bQAMera89fAUbezAOXvTEbwG95Mt7xU0jnL4mMZstj2s3Gj2RoCwL8CgrXAUatuwBeqjq0B8rJbaH6E8pQeX7vLz74PrMmaTu0KgGe63/UjorsWcAO+N6UWM8A2H7ndBXploOMXK6bVoZlt9z7fQuJVvUBbMc5kdDmuAL4AAbr3sOWvhz6mOJZuchoER9AmbPPsvfey7BZ2hSt7QQeV8AO8NmO8wDAy31+cYCi1TnIyIuAyequIrdm0SZtilYzAc/sdMz2iowKHeerggX918FfvRZYXrU4NxcZ7uOCOX/Kyu5bbWh3e/+SodUIUGo1Hw6Bj6QVogMM1r0A/93xoF7AEQX24LM6jkm7Y9Tcub7MWqXt0WomQFWOUOK3ZUNOanvhobUvvwPgs0vOErSk4HP4LI5daRsc234oq4ZpY7SKBpjee3wXRQ6Awow8I9a9Ajqd8baonpmLjEKo/gQyHjpo2evh7/x2BLSKBvCU+g9QIxVQkJFXj8PfASr61f9SVXrnIKMgzl+MjAb8dBjnL4TWMQEm8gr2fDtAVR4/4ssXfwqeODsXGe7jXMokkmFh7tp/5YyvM2qPdoRWMQGCHlwoFSyq90EgqAQc0pbOnythY6OnfFpmrdG+0OIa4Nkdj+6vSL8CqeDZh3/5n7kAtt/8TkXstnT+wjKEaYctfXl1lk3TLtDiGsDYVviRr3w1AHAvBPYTIvqbTMq0pAYIpq3xeUra9C0f+aDlfQDlQJXkT+UmSktyvFaa+SfAxtruhyvskEGZFtUAwYObD1v6cqu8278l0KIawIvXAsZCQTTAI0eseqE+kB6I+2dQpqU1QG1XX93dmbVG+0SLaoChlZ/upkZ6QgFGnglsrnih8og+xsjBOckoRD1cxyJ64V4rP2zIrlXaF1pUA6iRg8K/8xt5M47+8t8LABy1zgLiNpRke5yP1ggev3rw8tfa5OWOhUTLEgD5eYGcv/sh6PwpZxSiE3Mp4zpuciy7Vb/s0VJoMQIE7f8eoeM8OmBNqaf5OYB1yzc7QpFeBRi9+WqAa3+57KUlmbZFe0aLEWDnyppqYLN8O0BEHwlt+lCR32VSJpPjPGTMKu/beEs2bdGe0XImQEm49Ss6S/oG9zklDwM8U3VMb0WSRhSzPc5V9VvGnLnfW2/5s2qLdowWI4BRa0/IswOU947/8qkFADicTgGcPzeylqFy5S++fLUmi2Zo92hJJ3CvfEeesawHA+kIyukFst+5lRH5z2ErXuqwEb9kaBECPLrDqVsBAyCvkbeuvKnpaYBndxq3nyL9c5ARd5wjeb5sbi49TcAVAuwcaJFAkFNij1JEII/Ai/CPUOTPiHVmTjISHOdQpkksPfaYr59tl8/354sW0QAbSroO90nJEshdA4jRhwCe7Xv05oqMy0VGATSAqsjvfrnspTnZtkFHQYsQwKg1dL3VY10ezt+c41Y8Mw+gWUtPBrpkLSPJcTZlVOSaI5e/8LfcW6L9o4WcQKn2iWf3ZilZmEsHYEUeqhTR3xSi47OVIehfj1r+/PU53X4HQsEJ4MVrKQwBWG9vVh9Kz6ID6prt0icBpvcdv6siu7nlt4oGEHnmm622P5tNAAUnQEXlj/1BuijgxxrZQOnCbDpAkX/+euk/1gMYtaJ2/LaSBpj+3Zbbnnj23Ac6zN7+fFBwAnjEHqqEPGlhg92jAbLoAA286eOlqkPLgBMK5fxlIkPQJzfvu/bkTaXzoQUIYCzCnz9VwBF7tyarbEGGnbTwpBVPfAiw3t9jHEJP9/mW1ACC3vFx7YiTO1OYNxO0RByg2tWoAKyT7mZr1hCbnuD4gZAQRU4N/c5jDh/V4Uny+BDOG7f8uQeh3b7Mq8VQcA2gyNCg9g+PMCPW0Ear/DNXnkQjsdnnK/kbwBM7ntBLiez6KYQGSJLnR1H9xTHLn233r3JpKaQlwE0DLx2cqbDgHoABGhpbLiKss7qlVsGqz5/29WNrANQjJwN2Th585qr/Axtn5DErnn0z0/vrjEhLABV7+8kDL5t+U9UVW6fL22Pguu0VyoI75ggRQQVUrKFNVtknkLST/ho+VjklRw8+6bErzQGm/LjVlmPHHim3iwAADrJJREFU1T5Xm+g+pg28tGe6e+0sSEuAqxZPnQGyTizfZzcNvPToVHn94ukb+q2EVk4CakCBdXaP0iSd9G2vfqv+C/BY5SmjgF3c5wvo/C0T1X3H1z49KZmnf0fVhed6fH470bnOiIx8gKZm3+UgfsF6dvLAy+/39vWWJ8pnjO4UaezQvD5CBIMMabRKP4ntJIP1SMj7th3ntEKq/iB8wJ10Zfj4FU9/kKju0xlv3z7ggvvBdL9o+Z+/y6RdOgMyIoC39o61oH8I+uJnlZXWv3dz5cQ+sflE2Cn02x0LiBzDBqtHWSRPoJM8+B8FmF49vtRgFfolkm+L0V1/VTv9wuNrntqY6P7u6XPOFl9Xbf8sah2y1r+hQ7zcqVDIeBZwxZJpz6L6dKAjdaR6zKzJgy8d6c5jwm//jiCWCI7I4AYpm+fqpA9Oqn1iEUDzxtJfIvQshAYAPlWVY06sfXJseFdRAtxeddGejaWlcxU5QtVM8tY+0phpm3QGZDUN9JTK+cBaAIXt1Fgzbh5w6b6RHLp1oK8TjdwIEdZ5ulcAqggqEefPEfv0Ajh/i4xYp1m1ZsSJK558Ntm93Fl1ftltVRfeqPAuSD9g5sVf3NluX+veUsiKABNqbvlWkRtcjd7diPXSzQMn7gOAWNuFbX5MLCCEwHlrUL1dPg9oMGo9DfBw1RlbC3poIE/WGkBV5A1VOdZT6x968vLHH0v2VW4FubXqkvE+7BpFrlSwFZow1lkSclc2IWQdCfSVVNxV4qs/B+gf7ICuDubZG/pfspsqW4MggIbaUggeh5o3UGqjdOtRrs1Pn7HikbUAtt85SZGSQJHMoniKrEZ50hJzz0nLA2YkGbxjvZ5uq9YefquKF3Q4GiXHe+my2z/Nti06A7ImgLfG23z9oAmTRHnK1T09xfY8AbodRLz+kPsXGlbhTWIKjlC1xurqfr3baRl2/Ocq8jzw77LlTR8kG+khTBl04SBL7TP4ev2pirV9uHbBSxiVt3daurLT7PPPFpI+SzwU5MZBEz9CdVQiIRKjSaMNQPj428Ze3Xb0vuX1P9rv1GGq8klMeZ8iqxX53of9sVjyjlViXj1z8cNJP73m7eWt6NplY6URxqjoPhbsDTog8c0qoKtK/LrbpjTti0VOi0ECer3oZEGeAVCXanf/iozeULkonfCo9y2vH+Anu/vhJWre9Vmech92D0c8WzpYWwvaC+gF7AqcLoqZPPDyVaDuDqsX2AplB9i4mXGRLKkmCvzfgOpxm3LnQ44aAAJa4IZBkz4GHSauxGgFHrpIvEYQYw2dtHRKjXes1+P5puFLlO1jNETS8pE8GnPsTkpUj3AGn2CNu/zzaf9JfoebBnJeDRRQFW4HV7QvagoYQQIPfu6kpVNqAOxVTQcrbB8qG+ruVOUj10wQcQwnJaoHKOJDOKXY+QHktRy8ub/uiaCdBiKdoCEiSHRnhuf1oq6dtvprd9kwESQXIkg8EaIJWSeiR09YctsmN99PhrwIcMHSu5oUHgF358SOyLjO9Pn8+gTAlEETuqtwVOK1g4Rlib6WOy1+8SkiB0TkKyPsN2HJbS/lc8+dDXlvCFHLeixV6DeOCKKveL+Y9j1Ao9jHABWpyiUhUTBPsogjuIkA8obf59v9iiW3zs73fjsb8iaAd+FNnyGyIN0aQKRj7Ij6N6FPvCQvF1U2CREgWcSRDWCdP3HJLQddFfT2vX0v2jyf++1sKMiWMEWeCvxK6ngR7NB1FXVNLwLcOOTK7RH2z7CcKw0XEeLnB2EiKM8bx6m+YsnU/wvFIG8aePlh5VZZj0Lcc2dBYTaFiryiqtdAeI7tmnNHYgGK9eQlK29vAPDDCep63j82chCJKSQ6G7psMFWjcs4FJl75+S1vuKs4ecCE36M6eNKyKUUfwIWCEGBVxeq5vep61gFdFQ33UHgNAEK/wurfKL+WUDxW44I0cURwHyUMMYsswJEbrvz85ifdizo3DrlsKI411aC7lzpaTRFRyDkQFItrBl/xBsj+EaHRRABq/7R4cqWAequv2FkdCb9pI0rJa2xaID1RRYPXmSMqN/mXlP/bi9d4R3orrLrGIeI4e4nIUQL7A4LIr65aPGV6Ie61M6FgzwUo9jzB7B/4DYE2D55TBZFHw+uBjnWykiRULIEUjSJCvEkJljMgW4owzTOo4bbrmbg5Gxs2FwDLilwbHri62PkJkZQA51edX7aZZ4tTEYyjvucmL5qc+gUJyhcqyWy1qO23QupfFE4KnE+cP0KeWCLEmRQLqDShMsH0KMdQrHcdT/n5FJEQ6UyAXDn46j+JyFUCM0HewTLveUo8M70fe9e6M/5p8JWHgLwSLTzce+9du/CmnwFcNeSqfS3l7UQXTx7zJ7TilKDCKWP+c0p8HDhp2ZQO9RmX1kQ6E6A3Lbrh2qsHX/2pijwK+jOM4G9yuHrI1etRvgLWCrKFotsEi8SPVo18Q9eCE2PduGSriIG0AEIOo8YRIfHMQZH3Sks5ctLim4udnwIZxQFuWHTDv8AMBV6EcAP3UJFqhL1VdGeFnqGuCc3Pg53X1Fyi0wHOGnlWiaoclzJsS5oIX0wwyG02QtcV+Ic2lR90xac3/5Rle2xyyHoWcEW19zgxzq0CUdvCk3j+oOa56xffNA7g6uqrf4GRl935oiuRTM0nXlIOFHFTgA0icsGfFt30SFY3tQkj60jg5Brv0z9UfFelwimKhF+eFI7cScCTD41OseTJcGEjJ8bmc+eNaIQUEb6o6xHWCAgv2pa1S7Hzs0NO08CydWWWVWavMmqeV6QB9GexkbugF7/Bqih5AcDb11vejHN0XD4IzAsgydQvWTAofPa/BuO9ftHkD3O5l00dWRFg4uA/7mKLda4iJzrGbAapQ7iC/Ns711sP0FTOYYL0SBnqTRkDiDIPP4E8Jap3exffOD+beygiGhkRYMLQ64dY6tyIcrRRlUAHJvLkYwM2gXX/AJwTI6WSje7AuSREWA76GvCiZZe86q3xNmd1p0UkREoCXNz7ti4lPdZPFONMUgg80xf8AFRk/SXmOQDCawA/rK747jUAb7W3W5PRw6Li98H/E0T46hRWgsxHmC+WzjdGP7lx4Y0rCnC/RcQgKQEuH+o9XMyGu0D6BrttlQUzjPKBWLLIZzxf2Gb9j1MXT90wYdCE7pZWbC0WFyHm/KD6f/qB4CPYjWqOAqkA62XgNohMAiwx6oj+oLa9xvfjujW3B1cLi2gdxBHAO9JbUV9v36HGnKbwsiBTcKwZUxf/cXEyIVMXT90AbJg0+JoBIY3gIBH1r3JCQC3ooSLyyuQF3jtb4maKyB5RBJi483XVGxq5DPhYPLrjtPne7zMVdOlAb08sDXwkUuWrLgt5L3xSZJkKjqC2ordOHOL9eMpC7zuFuokickfYBztr5P0l3ZpWb3fbZ1d/lYugSTt7f6/BL3uKcMvNNd4J7vMTqq/dT1SfAbYQdMGPXb7Z9YFN6H187RXhQNADc8/25dr5AAZODP124PHY81NrrplhYR0KbFRk583re52X67WKKBwKsiFkwiBvL2z5CrAUXXTLAu+QZHkvG+I90BZeBtb+1GX7Xg/MPbuoBdoQhXlPoC0nKlgKiEjc6Hdj2kLv68BNCj23qv/moFR5i2h5FIQARjkhpEz8jqZ96qbLNlwvyCy/RMxGEW2DvAlw+dDr+2PJyOAiz+zbFnnTflDR+5bX7xf9jYjsnu/1i8gPeRPAqJyowcm/imvunwa31ngXqPJislfOFdE6KIAJ0BOCS7jGeCSrjZeNzXpdk6e0e/51KCJX5DULuHiX63YRI/MBFN68veaPBxSmWkW0FvLTAI51QujxLStq5a+IjoL8ngsQjgdQaG5yPEnfyVdE+0XOGuDi6htGK1IVXNF/9e5FV3bKDyt2duSsAVQY7zpIGfwpov0idwJgHRP8VeexS18oVIWKaF3kZAL+MPSGUQqVgSN5Ydr8y+sKWakiWg85EcASz3gIbwUvPnTZgZGTCVA1xwZDCBs93cteSZe/iPaLrDXAecOm7qZIfwBFX7z9w0uKe/g6MLLWAKJmPBJ8LYvydAvUqYhWRNYEUBgHIEJ9ueUpqv8OjqxMwLnVU3cFGRTcxf9i0fvv+MhOA4geF358Q0KvhiuiIyM7J1Dk2GDot74i8JBHER0cGRPgnKG3DFMYDGCwXiqq/86BjE2ACseFnvpDKar/ToKMNYARjgss/Uu9pRXFt212EmREgLNHTNtZYEjwrRyv3FNzXsIvcBbR8ZCRCVAjx4Z+W2hR/XciZKQBfMY/zsGg0FjSpayo/jsR0mqAk4bcsJMfZ1dLBQvz8l2zLlvfGhUronWQlgDGco5RVbFEELGL6r+TIS0BmnDG2SiWSlNJg7/4pa1OhpQEGDfMu43j+PZSVWzklelLryuq/06GlASo9zUeaQu2JYqR4tJvZ0RKAvjFf4xBsFR9HkuK6r8TIikB9h40obtffftZCJbom69+Oq344uVOiKQEMNJwuF+l3BJBoPjUTydFUgL4xYwTBUvFlCDPt2alimg9JHw6uKrq/LJudtNqC6u7JfLBnEX37t3aFSuidZBQA5RRf7Bf6W6JYhXVf6dGQgI4thmHgqiiYhfVfydGgsWg8TbKEQCq+tmiRQ+mfedPER0XcQQYXNVtH6AnAMK/WrtCRbQu4jWAJUeGftqqRfvfyRFPANUjgr9qaz5/5JPWrU4RrY0oAuxcdVo1woDg4bNEf82tiE6IKAIYS46KnCiq/00BMSZAQup/9YIldR+0em2KaHWECTC032+3BUYDILwITzltVakiWg9hAvhL/EeGjkWl+M6fTQQuExCe/jUZ4fU2qU0RrQ4LYGSvsyoQ9gdQeGPx4oc3tG21imgtWAD13fyHoFQAUFT/mxQsAMWE1L86ar3YhvUpopVhwXgb5JcAKG8vXfrQyjauUxGtCGvQoIo9gK0BI1jXtHWFimhdWBZyFIGP+k5c+Plfih9z3MTgUZVtVfSwxUseKb7xaxPE/wNdTWzU9o0tSgAAAABJRU5ErkJggg==';
1383     base_image.onload = function(){
1384         ctx.globalAlpha = 0.15
1385         ctx.drawImage(base_image, (canvas.width/2) - 64 - (lwidth/2), (canvas.height/2) - 128);
1386         ctx.globalAlpha = 1
1387     }
1388 }
1389
1390
1391
1392 /* Function for drawing line charts
1393  * Example usage:
1394  * 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" } );
1395  */
1396 function quokkaBars(id, titles, values, options) {
1397     var canvas = document.getElementById(id);
1398     var ctx=canvas.getContext("2d");
1399     // clear the canvas first
1400     ctx.clearRect(0, 0, canvas.width, canvas.height);
1401     var lwidth = 150;
1402     var lheight = 75;
1403     var stack = options ? options.stack : false;
1404     var astack = options ? options.astack : false;
1405     var curve = options ? options.curve : false;
1406     var title = options ? options.title : null;
1407     var noX = options ? options.nox : false;
1408     var verts = options ? options.verts : true;
1409     if (noX) {
1410         lheight = 0;
1411     }
1412     
1413     
1414     
1415     // Draw a border
1416     ctx.lineWidth = 0.5;
1417     ctx.strokeRect(25, 30, canvas.width - lwidth - 40, canvas.height - lheight - 40);
1418     
1419     // Draw a title if set:
1420     if (title != null) {
1421         ctx.font="15px Arial";
1422         ctx.fillStyle = "#000";
1423         ctx.textAlign = "center";
1424         ctx.fillText(title,(canvas.width-lwidth)/2, 15);
1425     }
1426     
1427     // Draw legend
1428     ctx.textAlign = "left";
1429     var posY = 50;
1430     for (var k in titles) {
1431         var x = parseInt(k)
1432         if (!noX) {
1433             x = x + 1;
1434         }
1435         var title = titles[k];
1436         if (title && title.length > 0) {
1437             ctx.fillStyle = colors[k % colors.length][0];
1438             ctx.fillRect(canvas.width - lwidth + 20, posY-10, 10, 10);
1439             
1440             // Add legend text
1441             ctx.font="12px Arial";
1442             ctx.fillStyle = "#000";
1443             ctx.fillText(title,canvas.width - lwidth + 40, posY);
1444             
1445             posY += 15;
1446         }
1447         
1448
1449     }
1450     
1451     // Find max and min
1452     var max = null;
1453     var min = 0;
1454     var stacked = null;
1455     for (x in values) {
1456         var s = 0;
1457         for (y in values[x]) {
1458             if (y > 0 || noX) {
1459                 s += values[x][y];
1460                 if (max == null || max < values[x][y]) {
1461                     max = values[x][y];
1462                 }
1463                 if (min == null || min > values[x][y]) {
1464                     min = values[x][y];
1465                 }
1466             }
1467         }
1468         if (stacked == null || stacked < s) {
1469             stacked = s;
1470         }
1471     }
1472     if (min == max) {
1473         max++;
1474     }
1475     if (stack) {
1476         min = 0;
1477         max = stacked;
1478     }
1479     
1480     
1481     // Set number of lines to draw and each step
1482     var numLines = 5;
1483     var step = (max-min) / (numLines+1);
1484     
1485     // Prettify the max value so steps aren't ugly numbers
1486     if (step %1 != 0) {
1487         step = (Math.round(step+0.5));
1488         max = step * (numLines+1);
1489     }
1490     
1491     // Draw horizontal lines
1492     for (x = numLines; x >= 0; x--) {
1493         
1494         var y = 30 + (((canvas.height-40-lheight) / (numLines+1)) * (x+1));
1495         ctx.moveTo(25, y);
1496         ctx.lineTo(canvas.width - lwidth - 15, y);
1497         ctx.lineWidth = 0.25;
1498         ctx.stroke();
1499         
1500         // Add values
1501         ctx.font="10px Arial";
1502         ctx.fillStyle = "#000";
1503         ctx.textAlign = "right";
1504         ctx.fillText( Math.round( ((max-min) - (step*(x+1))) * 100 ) / 100,canvas.width - lwidth + 12, y-4);
1505         ctx.fillText( Math.round( ((max-min) - (step*(x+1))) * 100 ) / 100,20, y-4);
1506     }
1507     
1508     
1509     // Draw vertical lines
1510     var sx = 1
1511     var numLines = values.length-1;
1512     var step = (canvas.width - lwidth - 40) / values.length;
1513     while (step < 24) {
1514         step *= 2
1515         sx *= 2
1516     }
1517     
1518     
1519     if (verts) {
1520         ctx.beginPath();
1521         for (var x = 1; x < values.length; x++) {
1522             if (x % sx == 0) {
1523                 var y = 35 + (step * (x/sx));
1524                 ctx.moveTo(y, 30);
1525                 ctx.lineTo(y, canvas.height - 10 - lheight);
1526                 ctx.lineWidth = 0.25;
1527                 ctx.stroke();
1528             }
1529         }
1530     }
1531     
1532     
1533     
1534     // Some pre-calculations of steps
1535     var step = (canvas.width - lwidth - 48) / values.length;
1536     var smallstep = (step / titles.length) - 2;
1537     
1538     // Draw X values if noX isn't set:
1539     if (noX != true) {
1540         ctx.beginPath();
1541         for (var i = 0; i < values.length; i++) {
1542             smallstep = (step / (values[i].length-1)) - 2;
1543             zz = 1
1544             var x = 35 + ((step) * i);
1545             var y = canvas.height - lheight + 5;
1546             if (i % sx == 0) {
1547                 ctx.translate(x, y);
1548                 ctx.moveTo(0,0);
1549                 ctx.lineTo(0,-15);
1550                 ctx.stroke();
1551                 ctx.rotate(45*Math.PI/180);
1552                 ctx.textAlign = "left";
1553                 var val = values[i][0];
1554                 if (val.constructor.toString().match("Date()")) {
1555                     val = val.toDateString();
1556                 }
1557                 ctx.fillText(val.toString(), 0, 0);
1558                 ctx.rotate(-45*Math.PI/180);
1559                 ctx.translate(-x,-y);
1560             }
1561         }
1562         
1563     }
1564     
1565     
1566     
1567     
1568     // Draw each line
1569     var stacks = [];
1570     var pstacks = [];
1571     
1572     for (k in values) {
1573         smallstep = (step / (values[k].length)) - 2;
1574         stacks[k] = 0;
1575         pstacks[k] = canvas.height - 40 - lheight;
1576         var beginX = 0;
1577         for (i in values[k]) {
1578             if (i > 0 || noX) {
1579                 var z = parseInt(i);
1580                 var zz = z;
1581                 if (!noX) {
1582                     z = parseInt(i) + 1;
1583                     zz = z - 2;
1584                     if (z > values[k].length) {
1585                         break;
1586                     }
1587                 }
1588                 var value = values[k][i];
1589                 var title = titles[i];
1590                 var color = colors[zz % colors.length][1];
1591                 var fcolor = colors[zz % colors.length][2];
1592                 if (values[k][2] && values[k][2].toString().match(/^#.+$/)) {
1593                     color = values[k][2]
1594                     fcolor = values[k][2]
1595                     smallstep = (step / (values[k].length-2)) - 2;
1596                 }
1597                 var x = ((step) * k) + ((smallstep+2) * zz) + 5;
1598                 var y = canvas.height - 10 - lheight;
1599                 var mdiff = (max-min);
1600                 mdiff = (mdiff == 0) ? 1 : mdiff;
1601                 var height = ((canvas.height - 40 - lheight) / (mdiff)) * value * -1;
1602                 var width = smallstep - 2;
1603                 if (width <= 1) {
1604                     width = 1
1605                 }
1606                 if (stack) {
1607                     width = step - 10;
1608                     y -= stacks[k];
1609                     stacks[k] -= height;
1610                     x = (step * k) + 4;
1611                     if (astack) {
1612                         y = canvas.height - 10 - lheight;
1613                     }
1614                 }
1615                 
1616                         
1617                 // Draw bar
1618                 ctx.beginPath();
1619                 ctx.lineWidth = 2;
1620                 ctx.strokeStyle = color;
1621                 ctx.strokeRect(27 + x, y, width, height);
1622                 var alpha = 0.75
1623                 if (fcolor.r) {
1624                     ctx.fillStyle = 'rgba('+ [parseInt(fcolor.r*255),parseInt(fcolor.g*255),parseInt(fcolor.b*255),alpha].join(",") + ')';
1625                 } else {
1626                     ctx.fillStyle = fcolor;
1627                 }
1628                 ctx.fillRect(27 + x, y, width, height);
1629                 
1630             }
1631         }
1632         
1633
1634     }
1635 }
1636
1637
1638 ]==]
1639
1640
1641 status_css = [[
1642     html {
1643     font-size: 14px;
1644     position: relative;
1645     background: #272B30;
1646     }
1647
1648     body {
1649         background-color: #272B30;
1650         color: #000;
1651         margin: 0 auto;
1652         min-height: 100%;
1653         font-family: Arial, Helvetica, sans-serif;
1654         font-weight: normal;
1655     }
1656     
1657     .navbarLeft {
1658         background: linear-gradient(to bottom, #F8A900 0%,#D88900 100%);
1659         width: 200px;
1660         height: 30px;
1661         padding-top: 2px;
1662         font-size: 1.35rem;
1663         color: #FFF;
1664         border-bottom: 2px solid #000;
1665         float: left;
1666         text-align: center;
1667     }
1668     
1669     .navbarRight {
1670         background: linear-gradient(to bottom, #EFEFEF 0%,#EEE 100%);
1671         width: calc(100% - 240px);
1672         height: 28px;
1673         color: #333;
1674         border-bottom: 2px solid #000;
1675         float: left;
1676         font-size: 1.3rem;
1677         padding-top: 4px;
1678         text-align: left;
1679         padding-left: 40px;
1680     }
1681     
1682     .wrapper {
1683         width: 100%;
1684         float: left;
1685         background: #33363F;
1686         min-height: calc(100% - 80px);
1687         position: relative;
1688     }
1689     
1690     .serverinfo {
1691         float: left;
1692         width: 200px;
1693         height: calc(100% - 34px);
1694         background: #293D4C;
1695     }
1696     
1697     .skey {
1698         background: rgba(30,30,30,0.3);
1699         color: #C6E7FF;
1700         font-weight: bold;
1701         padding: 2px;
1702     }
1703     
1704     .sval {
1705         padding: 2px;
1706         background: rgba(30,30,30,0.3);
1707         color: #FFF;
1708         font-size: 0.8rem;
1709         border-bottom: 1px solid rgba(200,200,200,0.2);
1710     }
1711     
1712     .charts {
1713         padding: 0px;
1714         width: calc(100% - 220px);
1715         max-width: 1000px;
1716         min-height: 100%;
1717         margin: 0px auto;
1718         position: relative;
1719         float: left;
1720         margin-left: 20px;
1721     }
1722
1723     pre, code {
1724         font-family: "Courier New", Courier, monospace;
1725     }
1726
1727     strong {
1728         font-weight: bold;
1729     }
1730
1731     q, em, var {
1732         font-style: italic;
1733     }
1734     /* h1                     */
1735     /* ====================== */
1736     h1 {
1737         padding: 0.2em;
1738         margin: 0;
1739         border: 1px solid #405871;
1740         background-color: inherit;
1741         color: #036;
1742         text-decoration: none;
1743         font-size: 22px;
1744         font-weight: bold;
1745     }
1746
1747     /* h2                     */
1748     /* ====================== */
1749     h2 {
1750         padding: 0.2em 0 0.2em 0.7em;
1751         margin: 0 0 0.5em 0;
1752         text-decoration: none;
1753         font-size: 18px;
1754         font-weight: bold;
1755         text-align: center;
1756     }
1757
1758     #modules {
1759         margin-top:20px;
1760         display:none;
1761         width:400px;
1762     }
1763     
1764     .servers {
1765         
1766         width: 1244px;
1767         background: #EEE;
1768     }
1769
1770     tr:nth-child(odd) {
1771         background: #F6F6F6;
1772     }
1773     tr:nth-child(even) {
1774         background: #EBEBEB;
1775     }
1776     td {
1777         padding: 2px;
1778     }
1779     table {
1780         border: 1px solid #333;
1781         padding: 0px;
1782         margin: 5px;
1783         min-width: 360px;
1784         background: #999;
1785         font-size: 0.8rem;
1786     }
1787     
1788     canvas {
1789         background: #FFF;
1790         margin: 3px;
1791         text-align: center;
1792         padding: 2px;
1793         border-radius: 10px;
1794         border: 1px solid #999;
1795     }
1796     
1797     .canvas_wide {
1798         position: relative;
1799         width: 65%;
1800     }
1801     .canvas_narrow {
1802         position: relative;
1803         width: 27%;
1804     }
1805     
1806     a {
1807         color: #FFA;
1808     }
1809     
1810     .statsbox {
1811         border-radius: 3px;
1812         background: #3C3E47;
1813         min-width: 150px;
1814         height: 60px;
1815         float: left;
1816         margin: 15px;
1817         padding: 10px;
1818     }
1819     
1820     .btn {
1821         background: linear-gradient(to bottom, #72ca72 0%,#55bf55 100%);
1822         border-radius: 5px;
1823         color: #FFF;
1824         text-decoration: none;
1825         padding-top: 6px;
1826         padding-bottom: 6px;
1827         padding-left: 3px;
1828         padding-right: 3px;
1829         font-weight: bold;
1830         text-shadow: 1px 1px rgba(0,0,0,0.4);
1831         margin: 12px;
1832         float: left;
1833         clear: none;
1834     }
1835     
1836     .infobox_wrapper {
1837         float: left;
1838         min-width: 200px;
1839         margin: 10px;
1840     }
1841     .infobox_title {
1842         border-top-left-radius: 4px;
1843         border-top-right-radius: 4px;
1844         background: #FAB227;
1845         color: #FFF;
1846         border: 2px solid #FAB227;
1847         border-bottom: none;
1848         font-weight: bold;
1849         text-align: center;
1850         width: 100%;
1851     }
1852     .infobox {
1853         background: #222222;
1854         border: 2px solid #FAB227;
1855         border-top: none;
1856         color: #EFEFEF;
1857         border-bottom-left-radius: 4px;
1858         border-bottom-right-radius: 4px;
1859         float: left;
1860         width: 100%;
1861
1862     }
1863     
1864     
1865     .serverinfo ul {
1866         margin: 0px;
1867         padding: 0px;
1868         margin-top: 20px;
1869         list-style: none;
1870     }
1871     
1872     .serverinfo ul li .btn {
1873         width: calc(100% - 8px);
1874         margin: 0px;
1875         border: 0px;
1876         border-radius: 0px;
1877         padding: 0px;
1878         padding-top: 8px;
1879         padding-left: 8px;
1880         height: 24px;
1881         background: rgba(0,0,0,0.2);
1882         border-bottom: 1px solid rgba(100,100,100,0.3);
1883     }
1884     
1885     .serverinfo  ul li:nth-child(1)  {
1886         border-top: 1px solid rgba(100,100,100,0.3);
1887     }
1888     .serverinfo ul li .btn.active {
1889         background: rgba(30,30,50,0.2);
1890         border-left: 4px solid #27FAB2;
1891         padding-left: 4px;
1892         color: #FFE;
1893     }
1894     
1895     .serverinfo ul li .btn:hover {
1896         background: rgba(50,50,50,0.15);
1897         border-left: 4px solid #FAB227;
1898         padding-left: 4px;
1899         color: #FFE;
1900     }
1901 ]]