2013-09-23 16:13:38 -05:00
<!doctype html>
< html lang = "en" >
< head >
< meta charset = "UTF-8" >
< meta name = "referrer" content = "never" >
< meta http-equiv = "X-UA-Compatible" content = "IE=edge" >
< base target = "_blank" >
< link rel = "alternate" type = "application/rss+xml" title = "RSS Feed" href = "/feed/archivebot.rss" >
< link rel = "alternate" type = "application/atom+xml" title = "Atom Feed" href = "/feed/archivebot.atom" >
< link rel = "icon" type = "image/png" href = "/assets/favicon.png" >
< title > ArchiveBot dashboard 2.0< / title >
2015-04-07 07:38:40 +00:00
< script >
2015-04-07 07:48:50 +00:00
(function() {
2015-04-07 07:51:00 +00:00
// Framebust out if necessary
2015-04-07 07:48:50 +00:00
if(self != top) {
var target = self.location.href;
2015-04-07 07:51:00 +00:00
// Ugly archivebot.com-specific hack
2015-04-07 07:48:50 +00:00
target = target.replace(
2015-04-07 07:51:00 +00:00
"http://arshboard.at.ninjawedding.org:4567/",
2015-04-07 07:48:50 +00:00
"http://dashboard.at.ninjawedding.org/");
top.location = target;
}
})();
2015-04-07 07:38:40 +00:00
< / script >
2013-09-23 16:13:38 -05:00
< / head >
< body >
< style >
2014-10-09 07:49:16 +00:00
html {
/* Always show scrollbar to prevent jumpiness when filtering */
overflow-y: scroll;
}
2013-09-23 16:13:38 -05:00
html, body {
background-color: #D0C0AE;
font-family: Tahoma, Arial, sans-serif;
font-size: 13px;
}
2014-10-09 05:09:42 +00:00
#filter-box {
2014-10-09 08:14:13 +00:00
background-color: #eee;
2014-10-09 05:09:42 +00:00
border: 1px solid #999;
2014-10-09 08:14:13 +00:00
padding: 1px 3px 1px 3px;
2014-10-09 05:09:42 +00:00
font-size: 18px;
border-radius: 3px;
}
2014-10-09 05:20:11 +00:00
.button {
font-size: 18px;
}
2013-09-23 16:13:38 -05:00
.padded-page {
padding: 20px 27px 20px 27px;
}
2015-01-03 07:25:07 +00:00
@media all and (min-width: 1440px) {
.padded-page {
padding: 20px 54px 47px 54px;
}
}
2013-09-23 16:13:38 -05:00
.header {
font-family: Arial, sans-serif;
font-weight: bold;
font-size: 18px;
margin: 0 0 20px 0;
display: flex;
align-items: flex-end;
justify-content: space-between;
flex-flow: row nowrap;
}
.job-header {
display: flex;
align-items: flex-end;
justify-content: space-between;
flex-flow: row nowrap;
2015-04-20 13:39:15 +00:00
cursor: pointer;
2013-09-23 16:13:38 -05:00
}
.job-info {
white-space: nowrap;
overflow: hidden;
}
2015-03-27 07:56:04 +00:00
.job-info-done {
color: #767676;
}
.job-info-aborted {
color: #9B00D7 !important;
}
.job-info-fatal {
color: #DD0000 !important;
}
2014-10-09 07:04:46 +00:00
.inline-stat {
2014-10-09 05:32:47 +00:00
/* Needed for 'Align!' feature */
display: inline-block;
2014-10-09 05:56:58 +00:00
/* Needed to avoid extra vertical padding */
vertical-align: bottom;
2014-10-09 07:04:46 +00:00
/* Needed to avoid collapsing of leading space */
white-space: pre;
}
.job-url {
2013-09-23 16:13:38 -05:00
font-family: Arial, sans-serif;
font-size: 14px;
font-weight: bold;
text-decoration: none;
2015-03-27 07:56:04 +00:00
color: inherit;
2013-09-23 16:13:38 -05:00
}
2014-10-09 05:48:07 +00:00
.job-url-aligned {
2014-10-09 07:04:46 +00:00
width: 260px;
2014-10-09 05:48:07 +00:00
overflow: hidden;
text-overflow: ellipsis;
}
.job-note-aligned {
display: none;
}
2014-10-09 05:56:58 +00:00
.job-nick-aligned {
width: 60px;
overflow: hidden;
text-overflow: hidden;
}
2014-10-09 07:04:46 +00:00
.job-mb-aligned {
2014-10-09 08:19:09 +00:00
width: 70px;
2014-10-09 07:04:46 +00:00
overflow: hidden;
text-overflow: hidden;
text-align: right;
}
.job-responses-aligned {
width: 92px;
overflow: hidden;
text-overflow: hidden;
text-align: right;
}
.job-in-queue-aligned {
width: 92px;
overflow: hidden;
text-overflow: hidden;
text-align: right;
}
.job-delay-aligned {
width: 130px;
overflow: hidden;
text-overflow: hidden;
text-align: right;
}
2014-10-09 07:34:37 +00:00
.job-ignores {
font-weight: bold;
}
.job-igoff {
font-weight: normal !important;
}
2013-09-23 16:13:38 -05:00
.job-ident {
2014-10-09 07:14:33 +00:00
font-family: Tahoma, Arial, sans-serif;
2013-09-23 16:13:38 -05:00
margin: 0 1px 0 0;
2014-10-09 07:14:33 +00:00
padding: 0;
2013-09-23 16:13:38 -05:00
border: 0;
background-color: #D0C0AE;
2014-10-09 07:14:33 +00:00
color: #645444;
2013-09-23 16:13:38 -05:00
text-align: right;
}
.log-window {
2015-01-03 10:21:16 +00:00
transition: all 0.18s ease;
2013-09-23 16:13:38 -05:00
background-color: #FFF7E1;
overflow-y: scroll;
height: 192px;
border: 1px solid #999;
margin: 0 0 1em 0;
border-radius: 3px;
}
2015-01-03 10:21:16 +00:00
.log-window-hidden {
height: 0;
2015-01-03 10:50:42 +00:00
border-width: 0 1px 0 1px;
2015-01-03 10:21:16 +00:00
margin: 0;
}
2013-09-23 16:13:38 -05:00
.log-window-stopped {
border: 1px solid #222;
box-shadow: 2px 2px 4px #888;
}
2015-01-03 09:29:10 +00:00
.log-window-expanded {
height: 384px;
}
2013-09-23 16:13:38 -05:00
.line-normal {
white-space: pre;
width: 100%;
padding: 0 0 0 5px;
box-sizing: border-box;
}
.line-error {
white-space: pre;
width: 100%;
background-color: #FFB9B9;
padding: 0 0 0 5px;
box-sizing: border-box;
}
.line-warning {
white-space: pre;
width: 100%;
background-color: #F7DB7D;
padding: 0 0 0 5px;
box-sizing: border-box;
}
.line-redirect {
white-space: pre;
width: 100%;
background-color: #E7CEEA;
padding: 0 0 0 5px;
box-sizing: border-box;
}
.line-ignore {
white-space: pre;
width: 100%;
color: #999;
padding: 0 0 0 5px;
box-sizing: border-box;
}
.line-stdout {
white-space: pre;
width: 100%;
background-color: #DCD8CB;
padding: 0 0 0 5px;
box-sizing: border-box;
}
a {
color: #000;
text-decoration: none;
}
a.ignore {
color: #999 !important;
}
.underlined-a {
text-decoration: underline;
}
.bold {
font-weight: bold;
}
#help {
background-color: #FFF7E1;
font-family: Arial, sans-serif;
font-size: 14px;
border-radius: 5px;
padding: 0.01em 1em 0.01em 1em;
margin-bottom: 1em;
}
#help p {
padding: 0.20em 0 0.20em 0;
}
#help p a {
text-decoration: underline;
}
.undisplayed {
display: none;
}
< / style >
< div class = "padded-page" >
< div class = "header" >
< div >
< a href = "http://archiveteam.org/index.php?title=ArchiveBot" class = "underlined-a" > ArchiveBot< / a >
2015-03-27 11:44:33 +00:00
tracking < span id = "num-crawls" > 0< / span > crawls.
2014-09-21 12:08:48 -05:00
See also < a href = "pipelines" class = "underlined-a" > pipeline< / a > or < a href = "logs/recent" class = "underlined-a" > job< / a > reports.
2015-04-07 02:33:33 +00:00
Show: < input id = "filter-box" type = "text" size = "21" >
2014-10-09 09:01:13 +00:00
< input onclick = "ds.setFilter('');" type = "button" value = " All " class = "button" >
2015-01-01 06:33:09 +00:00
< input onclick = "ds.setFilter('^$');" type = "button" value = "None" class = "button" >
2015-03-28 02:34:20 +00:00
< small id = "promo" > < a href = "/beta" class = "underlined-a" > Try 3.0 Beta< / a > < / small >
2013-09-23 16:13:38 -05:00
< / div >
< div >
2014-10-09 05:32:47 +00:00
< input type = "button" onclick = "ds.toggleAlign()" class = "button" value = "Align!" >
2014-10-09 05:20:11 +00:00
< input type = "button" onclick = "ds.toggleHelp()" class = "button" value = "Help!" >
2013-09-23 16:13:38 -05:00
< / div >
< / div >
< div id = "critical-info" >
< noscript >
Need JavaScript (ES5+) and WebSocket -> TCP:4567
< / noscript >
< div id = "help" class = "undisplayed" >
< p >
This page shows all of the crawls that < a href = "http://archiveteam.org/index.php?title=ArchiveBot" > ArchiveBot< / a > is currently running.
< / p >
< p >
To pause scrolling, move your mouse inside a log window.
< / p >
2014-10-09 09:01:13 +00:00
< p >
2015-03-29 02:56:30 +00:00
To show just one job, click anywhere on its stats line.
2014-10-09 09:01:13 +00:00
< / p >
2015-03-28 02:28:00 +00:00
< p >
Keyboard shortcuts:
< / p >
< ul >
< li > < kbd > j< / kbd > - show next job window
< li > < kbd > k< / kbd > - show previous job window
< li > < kbd > a< / kbd > - show all job windows
< li > < kbd > n< / kbd > - hide all job windows
< li > < kbd > f< / kbd > - move focus to filter box
2015-04-09 20:56:36 +00:00
< li > < kbd > ?< / kbd > - show/hide help text
2015-03-28 02:28:00 +00:00
< / ul >
2015-03-27 07:56:04 +00:00
< p >
Color coding for the job stats line:
in progress,
< span class = "job-info-done" > finished normally< / span > ,
< span class = "job-info-aborted" > finished with abort< / span > ,
< span class = "job-info-fatal" > finished with fatal exception< / span > .
< / p >
2013-09-23 16:13:38 -05:00
< p >
To clear all finished jobs, reload the page.
< / p >
< p >
Mouse over the job start date or the response count for additional information.
< / p >
< p >
If your adblocker is enabled for this domain, you will see slower performance, and some URLs will not be displayed.
< / p >
< p >
2014-09-08 22:07:33 +00:00
To use ArchiveBot, drop by < a href = "http://chat.efnet.org:9090/?nick=&channels=%23archivebot&Login=Login" > #archivebot< / a > on EFNet. < a href = "http://archivebot.readthedocs.org/en/latest/" > Issue commands< / a > by typing them into the channel. You will need channel operator (@) or voice (+) status to issue archiving jobs; just ask for help or leave a message with the website you want to archive.
2013-09-23 16:13:38 -05:00
< / p >
< p >
These < a href = "https://github.com/ArchiveTeam/ArchiveBot/tree/master/db/ignore_patterns" > ignore sets< / a > are available for crawls. The < a href = "https://github.com/ArchiveTeam/ArchiveBot/blob/master/db/ignore_patterns/global.json" > global< / a > ignore set automatically applies to all crawls.
< / p >
< p >
2014-09-04 23:38:53 +00:00
GitHub: < a href = "https://github.com/ArchiveTeam/ArchiveBot" > ArchiveBot< / a > .
2013-09-23 16:13:38 -05:00
< / p >
< / div >
< / div >
< div id = "traffic" > < / div >
< div id = "logs" > < / div >
< / div >
< script >
"use strict";
var assert = function(condition, message) {
if(!condition) {
throw message || "Assertion failed";
}
};
var byId = function(id) {
return document.getElementById(id);
};
var text = function(s) {
return document.createTextNode(s);
};
2014-09-10 03:20:34 -05:00
/**
* Adaptation of ActiveSupport's #blank?.
*
* Returns true if the object is undefined, null, or is a string whose
* post-trim length is zero. Otherwise, returns false.
*/
var isBlank = function(o) {
2014-09-10 11:14:10 -05:00
return !o || o.trim().length === 0;
2014-09-10 03:20:34 -05:00
}
2013-09-23 16:13:38 -05:00
/**
* appendChild but accepts strings and arrays of children|strings
*/
var appendAny = function(e, thing) {
if(Array.isArray(thing)) {
for(var i=0; i < thing.length ; i + + ) {
appendAny(e, thing[i]);
}
} else if(typeof thing == "string") {
e.appendChild(text(thing));
} else {
2015-03-27 07:56:04 +00:00
if(thing == null) {
throw Error("thing is " + JSON.stringify(thing));
}
2013-09-23 16:13:38 -05:00
e.appendChild(thing);
}
};
/**
* Create DOM element with attributes and children from Array< node | string > |node|string
*/
var h = function(elem, attrs, thing) {
var e = document.createElement(elem);
if(attrs != null) {
for(var attr in attrs) {
if(attr == "spellcheck" || attr == "readonly") {
e.setAttribute(attr, attrs[attr]);
} else {
e[attr] = attrs[attr];
}
}
}
if(thing != null) {
appendAny(e, thing);
}
return e;
};
var href = function(href, text) {
var a = h("a");
a.href = href;
a.textContent = text;
return a;
};
var prettyJson = function(obj) {
return JSON.stringify(obj, undefined, 2);
};
// Copied from Coreweb/js_coreweb/cw/string.js
/**
* Like Python's s.split(delim, num) and s.split(delim)
* This does *NOT* implement Python's no-argument s.split()
*
* @param {string} s The string to split.
* @param {string} sep The separator to split by.
* @param {number} maxsplit Maximum number of times to split.
*
* @return {!Array.< string > } The splitted string, as an array.
*/
var split = function(s, sep, maxsplit) {
assert(typeof sep == "string",
"arguments[1] of split must be a separator string");
if(maxsplit === undefined || maxsplit < 0 ) {
return s.split(sep);
}
var pieces = s.split(sep);
var head = pieces.splice(0, maxsplit);
// after the splice, pieces is shorter and no longer has the `head` elements.
if(pieces.length > 0) {
var tail = pieces.join(sep);
head.push(tail); // no longer just the head.
}
return head;
};
2015-01-01 06:33:09 +00:00
// Copied from closure-library's goog.string.startsWith
var startsWith = function(str, prefix) {
return str.lastIndexOf(prefix, 0) == 0;
}
// Copied from closure-library's goog.string.endsWith
var endsWith = function(str, suffix) {
var l = str.length - suffix.length;
return l >= 0 & & str.indexOf(suffix, l) == l;
};
// Copied from closure-library's goog.string.regExpEscape
var regExpEscape = function(s) {
return String(s).replace(/([-()\[\]{}+?*.$\^|,:#< !\\])/g, '\\$1').
replace(/\x08/g, '\\x08');
};
2013-09-23 16:13:38 -05:00
/**
* [[1, 2], [3, 4]] -> {1: 2, 3: 4}
*/
var intoObject = function(arr) {
var obj = {};
arr.forEach(function(e) {
obj[e[0]] = e[1];
});
return obj;
};
var getQueryArgs = function() {
var pairs = location.search.replace("?", "").split("&");
if(pairs == "") {
return {};
}
return intoObject(pairs.map(function(e) { return split(e, "=", 1); }));
};
var isSafari = navigator.userAgent.indexOf("Safari") != -1;
2015-03-27 13:45:03 +00:00
var isFirefox = navigator.userAgent.indexOf("Firefox") != -1;
2013-09-23 16:13:38 -05:00
2014-10-09 05:09:42 +00:00
var addAnyChangeListener = function(elem, func) {
2014-10-09 07:49:16 +00:00
// DOM0 handler for convenient use by Clear button
elem.onchange = func;
2014-10-09 05:09:42 +00:00
elem.addEventListener('keydown', func, false);
elem.addEventListener('paste', func, false);
elem.addEventListener('input', func, false);
};
2014-10-09 05:48:07 +00:00
var arrayFrom = function(arrayLike) {
return Array.prototype.slice.call(arrayLike);
};
2014-10-09 05:09:42 +00:00
/**
* Returns a function that gets the given property on any object passed in
*/
var prop = function(name) {
return function(obj) {
return obj[name];
};
};
2014-10-09 05:48:07 +00:00
/**
* Returns a function that adds the given class to any element passed in
*/
var classAdder = function(name) {
return function(elem) {
elem.classList.add(name);
};
};
/**
* Returns a function that removes the given class to any element passed in
*/
var classRemover = function(name) {
return function(elem) {
elem.classList.remove(name);
};
};
2015-03-27 11:44:33 +00:00
var removeFromArray = function(arr, item) {
var idx = arr.indexOf(item);
if(idx != -1) {
arr.splice(idx, 1);
}
};
2015-03-27 12:11:25 +00:00
// Based on http://stackoverflow.com/a/18520276
var findInArray = function(arr, test, ctx) {
var result = null;
arr.some(function(el, i) {
return test.call(ctx, el, i, arr) ? ((result = i), true) : false;
});
return result;
};
2013-09-23 16:13:38 -05:00
/*** End of utility code ***/
var JobsTracker = function() {
this.known = {};
this.sorted = [];
this.finishedArray = [];
this.finishedSet = {};
2015-03-27 12:48:42 +00:00
this.fatalExceptionSet = {};
2013-09-23 16:13:38 -05:00
};
JobsTracker.prototype.countActive = function() {
return this.sorted.length - this.finishedArray.length;
};
JobsTracker.prototype.resort = function() {
this.sorted.sort(function(a, b) { return a["started_at"] > b["started_at"] ? -1 : 1 });
};
/**
* Returns true if a new job was added
*/
JobsTracker.prototype.handleJobData = function(jobData) {
var ident = jobData["ident"];
var alreadyKnown = ident in this.known;
if(!alreadyKnown) {
this.known[ident] = true;
this.sorted.push(jobData);
this.resort();
}
2015-03-27 11:44:33 +00:00
return !alreadyKnown;
};
2015-03-27 12:48:42 +00:00
JobsTracker.prototype.markFinished = function(ident) {
2015-03-27 11:44:33 +00:00
if(!(ident in this.finishedSet)) {
2013-09-23 16:13:38 -05:00
this.finishedSet[ident] = true;
this.finishedArray.push(ident);
}
2015-03-27 11:44:33 +00:00
};
2015-03-27 12:48:42 +00:00
JobsTracker.prototype.markUnfinished = function(ident) {
2015-03-27 11:44:33 +00:00
if(ident in this.finishedSet) {
delete this.finishedSet[ident];
removeFromArray(this.finishedArray, ident);
}
2015-03-27 12:48:42 +00:00
// Job was restarted, so unmark fatal exception
if(ident in this.fatalExceptionSet) {
delete this.fatalExceptionSet[ident];
}
};
JobsTracker.prototype.markFatalException = function(ident) {
this.fatalExceptionSet[ident] = true;
};
JobsTracker.prototype.hasFatalException = function(ident) {
return ident in this.fatalExceptionSet;
2013-09-23 16:13:38 -05:00
};
2014-09-10 02:26:28 -05:00
var JobRenderInfo = function(logWindow, logSegment, statsElements, jobNote, lineCountWindow, lineCountSegments) {
2013-09-23 16:13:38 -05:00
this.logWindow = logWindow;
this.logSegment = logSegment;
this.statsElements = statsElements;
2014-09-10 02:26:28 -05:00
this.jobNote = jobNote;
2013-09-23 16:13:38 -05:00
this.lineCountWindow = lineCountWindow;
this.lineCountSegments = lineCountSegments;
};
var Reusable = {
obj_className_line_normal: {"className": "line-normal"},
obj_className_line_error: {"className": "line-error"},
obj_className_line_warning: {"className": "line-warning"},
obj_className_line_redirect: {"className": "line-redirect"},
obj_className_line_ignore: {"className": "line-ignore"},
obj_className_line_stdout: {"className": "line-stdout"},
obj_className_bold: {"className": "bold"}
};
// http://stackoverflow.com/questions/2901102/how-to-print-a-number-with-commas-as-thousands-separators-in-javascript
2014-09-10 23:27:27 +00:00
var numberWithCommas = function(s_or_n) {
return ("" + s_or_n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
2013-09-23 16:13:38 -05:00
};
var toStringTenths = function(n) {
var s = "" + (Math.round(10 * n) / 10);
if(s.indexOf(".") == -1) {
s += ".0";
}
return s;
};
var getTotalResponses = function(jobData) {
return (
parseInt(jobData["r1xx"]) +
parseInt(jobData["r2xx"]) +
parseInt(jobData["r3xx"]) +
parseInt(jobData["r4xx"]) +
parseInt(jobData["r5xx"]) +
parseInt(jobData["runk"]));
};
var getSummaryResponses = function(jobData) {
return (
2014-09-10 23:27:27 +00:00
"1xx: " + numberWithCommas(jobData["r1xx"]) + "\n" +
"2xx: " + numberWithCommas(jobData["r2xx"]) + "\n" +
"3xx: " + numberWithCommas(jobData["r3xx"]) + "\n" +
"4xx: " + numberWithCommas(jobData["r4xx"]) + "\n" +
"5xx: " + numberWithCommas(jobData["r5xx"]) + "\n" +
"Unknown: " + numberWithCommas(jobData["runk"]));
2013-09-23 16:13:38 -05:00
};
2014-10-09 05:09:42 +00:00
var JobsRenderer = function(container, filterBox, historyLines, showNicks) {
2013-09-23 16:13:38 -05:00
this.container = container;
2014-10-09 05:09:42 +00:00
this.filterBox = filterBox;
2014-10-09 08:23:30 +00:00
addAnyChangeListener(this.filterBox, this.applyFilter.bind(this));
2015-03-27 11:28:33 +00:00
this.filterBox.onkeypress = function(ev) {
// So that j or k in input box does not result in job window switching
ev.stopPropagation();
}
2013-09-23 16:13:38 -05:00
this.historyLines = historyLines;
this.showNicks = showNicks;
this.linesPerSegment = Math.max(1, Math.round(this.historyLines / 10));
this.jobs = new JobsTracker();
// ident -> JobRenderInfo
this.renderInfo = {};
this.mouseInside = null;
this.numCrawls = byId('num-crawls');
2014-10-09 06:05:49 +00:00
this.aligned = false;
2013-09-23 16:13:38 -05:00
};
JobsRenderer.prototype._getNextJobInSorted = function(ident) {
for(var i=0; i < this.jobs.sorted.length ; i + + ) {
var e = this.jobs.sorted[i];
if(e["ident"] == ident) {
return this.jobs.sorted[i+1];
}
}
return null;
};
JobsRenderer.prototype._createLogSegment = function() {
return h('div');
};
JobsRenderer.prototype._createLogContainer = function(jobData) {
var ident = jobData["ident"];
var beforeJob = this._getNextJobInSorted(ident);
var beforeElement = beforeJob == null ? null : byId("log-container-" + beforeJob["ident"]);
var logSegment = this._createLogSegment();
var logWindowAttrs = {
"className": "log-window",
"id": "log-window-" + ident,
"onmouseenter": function(ev) {
this.mouseInside = ident;
ev.target.classList.add('log-window-stopped');
}.bind(this),
"onmouseleave": function(ev) {
this.mouseInside = null;
ev.target.classList.remove('log-window-stopped');
}.bind(this)
}
// If you reach the end of a log window, the browser annoyingly
// starts to scroll the page instead. We prevent this behavior here.
// If the user wants to scroll the page, they need to move their
// mouse outside a log window first.
if(!isSafari) {
logWindowAttrs["onwheel"] = function(ev) {
// Note: offsetHeight is "wrong" by 2px but it doesn't matter
//console.log(ev, logWindow.scrollTop, (logWindow.scrollHeight - logWindow.offsetHeight));
if(ev.deltaY < 0 & & logWindow . scrollTop = = 0 ) {
ev.preventDefault();
} else if(ev.deltaY > 0 & & logWindow.scrollTop >= (logWindow.scrollHeight - logWindow.offsetHeight)) {
ev.preventDefault();
}
}
} else {
// Safari 7.0.5 can't preventDefault or stopPropagation an onwheel event,
// so use onmousewheel instead.
logWindowAttrs["onmousewheel"] = function(ev) {
//console.log(ev, logWindow.scrollTop, (logWindow.scrollHeight - logWindow.offsetHeight));
if(ev.wheelDeltaY > 0 & & logWindow.scrollTop == 0) {
ev.preventDefault();
} else if(ev.wheelDeltaY < 0 & & logWindow . scrollTop > = (logWindow.scrollHeight - logWindow.offsetHeight)) {
ev.preventDefault();
}
}
}
var statsElements = {
2014-10-09 08:19:09 +00:00
mb: h("span", {"className": "inline-stat job-mb"}, "?"),
2014-10-09 07:04:46 +00:00
responses: h("span", {"className": "inline-stat job-responses"}, "?"),
2013-09-23 16:13:38 -05:00
responsesPerSecond: h("span", null, "?"),
2014-10-09 07:04:46 +00:00
queueLength: h("span", {"className": "inline-stat job-in-queue"}, "? in q."),
2013-09-23 16:13:38 -05:00
connections: h("span", null, "?"),
2014-10-09 07:04:46 +00:00
delay: h("span", {"className": "inline-stat job-delay"}, "? ms delay"),
2015-03-27 07:56:04 +00:00
ignores: h("span", {"className": "job-ignores"}, "?"),
jobInfo: null /* set later */
2013-09-23 16:13:38 -05:00
};
2015-03-27 07:56:04 +00:00
var startedISOString = new Date(parseFloat(jobData["started_at"]) * 1000).toISOString();
2014-10-09 05:48:07 +00:00
var jobNote = h("span", {"className": "job-note"}, null);
2014-09-10 02:26:28 -05:00
2015-03-27 07:56:04 +00:00
statsElements.jobInfo = h(
"span", {"className": "job-info"}, [
h("a", {"className": "inline-stat job-url", "href": jobData["url"]}, jobData["url"]),
2015-03-27 08:16:21 +00:00
// Clicking anywhere in this area will set the filter to a regexp that
// matches only this job URL, thus hiding everything but this job.
2015-03-27 07:56:04 +00:00
h("span", {
"onclick": function() {
2015-03-27 13:49:00 +00:00
if(window.getSelection().toString().length > 1) {
// Set the filter on clicks, but not on text selections (unless just one character).
2015-03-27 07:56:04 +00:00
return;
}
var filter = ds.getFilter();
if(RegExp(filter).test(jobData["url"]) & & startsWith(filter, "^") & & endsWith(filter, "$")) {
// If we're already showing just this log window, go back
// to showing nothing.
ds.setFilter("^$");
} else {
ds.setFilter("^" + regExpEscape(jobData["url"]) + "$");
}
}
}, [
" on ",
h("span", {"title": startedISOString}, startedISOString.split("T")[0].substr(5)),
h("span", {"className": "inline-stat job-nick"}, (this.showNicks ? " by " + jobData["started_by"] : "")),
jobNote,
"; ",
statsElements.mb,
" MB in ",
statsElements.responses,
" at ",
statsElements.responsesPerSecond,
"/s, ",
statsElements.queueLength,
"; ",
statsElements.connections,
" con. w/ ",
statsElements.delay,
"; ",
statsElements.ignores
])
]
);
2013-09-23 16:13:38 -05:00
var logWindow = h('div', logWindowAttrs, logSegment);
var div = h(
'div',
{"id": "log-container-" + ident}, [
h("div", {"className": "job-header"}, [
2015-03-27 07:56:04 +00:00
statsElements.jobInfo,
2013-09-23 16:13:38 -05:00
h("input", {
"className": "job-ident",
"type": "text",
"value": ident,
"size": "28",
"spellcheck": "false",
"readonly": "",
"onclick": function() { this.select(); }
})
]),
logWindow
]
);
2014-09-10 02:26:28 -05:00
this.renderInfo[ident] = new JobRenderInfo(logWindow, logSegment, statsElements, jobNote, 0, [0]);
2013-09-23 16:13:38 -05:00
this.container.insertBefore(div, beforeElement);
2014-10-09 07:56:52 +00:00
// Set appropriate CSS classes - we might be in aligned mode already
this.updateAlign();
2015-01-03 09:29:10 +00:00
// Filter hasn't changed, but we might need to filter out the new job, or
// add/remove log-window-expanded class
2014-10-09 08:23:30 +00:00
this.applyFilter();
2013-09-23 16:13:38 -05:00
}
JobsRenderer.prototype._renderDownloadLine = function(data, logSegment) {
if(data["is_warning"]) {
var attrs = Reusable.obj_className_line_warning;
} else if(data["is_error"]) {
var attrs = Reusable.obj_className_line_error;
} else if(data["response_code"] & & data["response_code"] >= 300 & & data["response_code"] < 400 ) {
var attrs = Reusable.obj_className_line_redirect;
} else {
var attrs = Reusable.obj_className_line_normal;
}
logSegment.appendChild(h("div", attrs, [
data["response_code"] + " " + data["wget_code"] + " ",
href(data["url"], data["url"])
]));
return 1;
};
JobsRenderer.prototype._renderIgnoreLine = function(data, logSegment) {
var attrs = Reusable.obj_className_line_ignore;
logSegment.appendChild(h("div", attrs, [
h('span', null, " IGNOR "),
h('a', {"href": data["url"], "className": "ignore"}, data["url"]),
h('span', Reusable.obj_className_bold, " by "),
data["pattern"]
]));
return 1;
};
2015-03-27 11:44:33 +00:00
JobsRenderer.prototype._renderStdoutLine = function(data, logSegment, info, ident) {
2013-09-23 16:13:38 -05:00
var cleanedMessage = data["message"].replace(/[\r\n]+$/, "");
var renderedLines = 0;
if(!cleanedMessage) {
return renderedLines;
}
var lines = cleanedMessage.split("\n");
for(var i=0; i < lines.length ; i + + ) {
var line = lines[i];
if(!line) {
continue;
}
logSegment.appendChild(h("div", Reusable.obj_className_line_stdout, line));
renderedLines += 1;
2015-03-27 07:56:04 +00:00
2015-03-27 09:27:31 +00:00
// Check for 'Finished RsyncUpload for Item'
// instead of 'Starting MarkItemAsDone for Item'
// because the latter is often missing
if(/^Finished RsyncUpload for Item/.test(line)) {
2015-03-27 07:56:04 +00:00
info.statsElements.jobInfo.classList.add('job-info-done');
2015-03-27 11:44:33 +00:00
this.jobs.markFinished(ident);
2015-04-18 15:30:06 +00:00
} else if(/^CRITICAL (Sorry|Please report)|^ERROR Fatal exception|No space left on device|^Call stack:|^--- Logging error ---|^Fatal Python error:|^(Thread|Current thread) 0x/.test(line)) {
2015-03-27 07:56:04 +00:00
info.statsElements.jobInfo.classList.add('job-info-fatal');
2015-03-27 12:48:42 +00:00
this.jobs.markFatalException(ident);
2015-04-18 19:47:04 +00:00
} else if(/Script requested immediate stop|^Adjusted target WARC path to.*-aborted$/.test(line)) {
2015-03-27 08:04:24 +00:00
// Note: above message can be in:
// ERROR Script requested immediate stop
// or after an ERROR Fatal exception:
// wpull.hook.HookStop: Script requested immediate stop.
2015-04-18 19:47:04 +00:00
//
// Also check for "Adjusted target WARC path" because
// the exception may be entirely missing.
2015-03-27 08:04:24 +00:00
info.statsElements.jobInfo.classList.remove('job-info-fatal');
2015-03-27 07:56:04 +00:00
info.statsElements.jobInfo.classList.add('job-info-aborted');
} else if(/^Received item /.test(line)) {
2015-03-27 08:04:24 +00:00
// Clear other statuses if a job restarts with the same job ID
2015-03-27 07:56:04 +00:00
info.statsElements.jobInfo.classList.remove('job-info-done');
info.statsElements.jobInfo.classList.remove('job-info-fatal');
info.statsElements.jobInfo.classList.remove('job-info-aborted');
2015-03-27 11:44:33 +00:00
this.jobs.markUnfinished(ident);
2015-03-27 07:56:04 +00:00
}
2013-09-23 16:13:38 -05:00
}
return renderedLines;
};
JobsRenderer.prototype.handleData = function(data) {
var jobData = data["job_data"];
var added = this.jobs.handleJobData(jobData);
this.numCrawls.textContent = this.jobs.countActive();
if(added) {
this._createLogContainer(jobData);
}
var type = data["type"];
var ident = jobData["ident"];
var info = this.renderInfo[ident];
if(!info) {
console.warn("No render info for " + ident);
return;
}
var totalResponses = parseInt(getTotalResponses(jobData));
if(type == "download") {
var linesRendered = this._renderDownloadLine(data, info.logSegment);
} else if(type == "stdout") {
2015-03-27 11:44:33 +00:00
var linesRendered = this._renderStdoutLine(data, info.logSegment, info, ident);
2013-09-23 16:13:38 -05:00
} else if(type == "ignore") {
var linesRendered = this._renderIgnoreLine(data, info.logSegment);
} else {
assert(false, "Unexpected message type " + type);
}
// Update stats
info.statsElements.mb.textContent =
numberWithCommas(
toStringTenths(
(parseInt(jobData["bytes_downloaded"]) / (1024 * 1024)).toString()));
info.statsElements.responses.textContent =
2014-09-10 23:27:27 +00:00
numberWithCommas(totalResponses) + " resp.";
2013-09-23 16:13:38 -05:00
info.statsElements.responses.title = getSummaryResponses(jobData);
var duration = Date.now()/1000 - parseFloat(jobData["started_at"]);
info.statsElements.responsesPerSecond.textContent =
toStringTenths(totalResponses/duration);
2014-09-10 00:06:17 -05:00
if (jobData["items_queued"] & & jobData["items_downloaded"]) {
var totalQueued = parseInt(jobData["items_queued"], 10);
var totalDownloaded = parseInt(jobData["items_downloaded"], 10);
info.statsElements.queueLength.textContent =
2014-09-10 23:27:27 +00:00
numberWithCommas((totalQueued - totalDownloaded) + " in q.");
2014-09-10 00:06:17 -05:00
info.statsElements.queueLength.title =
2014-09-10 23:27:27 +00:00
numberWithCommas(totalQueued) + " queued\n" +
numberWithCommas(totalDownloaded) + " downloaded";
2014-09-10 00:06:17 -05:00
}
2013-09-23 16:13:38 -05:00
info.statsElements.connections.textContent = jobData["concurrency"];
2014-10-09 08:21:33 +00:00
2013-09-23 16:13:38 -05:00
var delayMin = parseInt(jobData["delay_min"]);
var delayMax = parseInt(jobData["delay_max"]);
info.statsElements.delay.textContent =
2014-10-09 07:04:46 +00:00
(delayMin == delayMax ?
2013-09-23 16:13:38 -05:00
delayMin :
2014-10-09 07:04:46 +00:00
delayMin + "-" + delayMax) + " ms delay";
2014-10-09 08:21:33 +00:00
2014-10-09 07:34:37 +00:00
if(jobData["suppress_ignore_reports"]) {
2014-10-09 08:21:33 +00:00
info.statsElements.ignores.textContent = 'igoff';
2014-10-09 07:34:37 +00:00
if(!info.statsElements.ignores.classList.contains('job-igoff')) {
info.statsElements.ignores.classList.add('job-igoff');
}
} else {
2014-10-09 08:21:33 +00:00
info.statsElements.ignores.textContent = 'igon';
2014-10-09 07:34:37 +00:00
if(info.statsElements.ignores.classList.contains('job-igoff')) {
info.statsElements.ignores.classList.remove('job-igoff');
}
}
2013-09-23 16:13:38 -05:00
2014-09-10 02:26:28 -05:00
// Update note
2014-09-10 23:29:23 +00:00
info.jobNote.textContent =
isBlank(jobData["note"]) ?
2014-09-10 03:20:34 -05:00
"" :
" (" + jobData["note"] + ")";
2014-09-10 02:26:28 -05:00
2013-09-23 16:13:38 -05:00
info.lineCountWindow += linesRendered;
info.lineCountSegments[info.lineCountSegments.length - 1] += linesRendered;
if(info.lineCountSegments[info.lineCountSegments.length - 1] >= this.linesPerSegment) {
//console.log("Created new segment", info);
var newSegment = this._createLogSegment();
info.logWindow.appendChild(newSegment);
info.logSegment = newSegment;
info.lineCountSegments.push(0);
}
if(this.mouseInside != ident) {
2015-03-27 12:48:42 +00:00
// Don't remove any scrollback information when the job has a fatal exception,
// so that the user can find the traceback and report a bug.
if(!this.jobs.hasFatalException(ident)) {
// We may have to remove more than one segment, if the user
// has paused the log window for a while.
while(info.lineCountWindow >= this.historyLines + this.linesPerSegment) {
var firstLogSegment = info.logWindow.firstChild;
assert(firstLogSegment != null, "info.logWindow.firstChild is null; " +
JSON.stringify({
"lineCountWindow": info.lineCountWindow,
"lineCountSegments": info.lineCountSegments}));
info.logWindow.removeChild(firstLogSegment);
info.lineCountWindow -= info.lineCountSegments[0];
info.lineCountSegments.shift();
}
2013-09-23 16:13:38 -05:00
}
// Scroll to the bottom
2015-03-27 08:50:24 +00:00
// To avoid serious performance problems in Firefox, we use a big number
// instead of info.logWindow.scrollHeight.
info.logWindow.scrollTop = 999999;
2013-09-23 16:13:38 -05:00
}
};
2014-10-09 08:23:30 +00:00
JobsRenderer.prototype.applyFilter = function() {
2014-10-09 05:09:42 +00:00
var query = this.filterBox.value;
2014-10-09 06:05:49 +00:00
var matches = 0;
2015-01-03 09:29:10 +00:00
var matchedWindows = [];
var unmatchedWindows = [];
2015-03-27 12:11:25 +00:00
this.firstFilterMatch = null;
2014-10-09 05:09:42 +00:00
for(var i=0; i < this.jobs.sorted.length ; i + + ) {
var job = this.jobs.sorted[i];
2015-01-03 09:29:10 +00:00
var w = this.renderInfo[job["ident"]].logWindow;
2015-01-01 06:33:09 +00:00
if(!RegExp(query).test(job["url"])) {
2015-01-03 10:21:16 +00:00
w.classList.add("log-window-hidden");
2015-03-27 13:45:03 +00:00
// Firefox exhibits serious performance problems when adding
// lines to our 0px-high log windows, so add display: none
// (effectively killing the animation)
if(isFirefox) {
w.style.display = "none";
}
// Remove this class, else an ugly border may be visible
2015-01-03 11:20:37 +00:00
w.classList.remove('log-window-stopped');
2015-01-03 09:29:10 +00:00
unmatchedWindows.push(w);
2014-10-09 05:09:42 +00:00
} else {
2015-01-03 10:21:16 +00:00
w.classList.remove("log-window-hidden");
2015-03-27 13:45:03 +00:00
if(isFirefox) {
w.style.display = "block";
}
2014-10-09 06:05:49 +00:00
matches += 1;
2015-01-03 09:29:10 +00:00
matchedWindows.push(w);
2015-03-27 13:45:03 +00:00
if(this.firstFilterMatch == null) {
this.firstFilterMatch = job["ident"];
}
2014-10-09 05:09:42 +00:00
}
}
2014-10-09 06:07:36 +00:00
2015-01-03 09:29:10 +00:00
// If there's only one visible log window, expand it so that more lines are visible.
unmatchedWindows.map(classRemover('log-window-expanded'));
matchedWindows.map(classRemover('log-window-expanded'));
if(matches == 1) {
matchedWindows.map(classAdder('log-window-expanded'));
}
2014-10-09 06:05:49 +00:00
if(matches < this.jobs.sorted.length ) {
2014-10-09 06:07:36 +00:00
// If you're not seeing all of the log windows, you're probably seeing very
// few of them, so you probably want alignment enabled.
2014-10-09 06:05:49 +00:00
this.aligned = true;
this.updateAlign();
} else {
2014-10-09 06:07:36 +00:00
// You're seeing all of the log windows, so alignment doesn't help as much
// as seeing the full info.
2014-10-09 06:05:49 +00:00
this.aligned = false;
this.updateAlign();
}
};
2015-03-27 12:11:25 +00:00
JobsRenderer.prototype.showNextPrev = function(offset) {
var idx = findInArray(this.jobs.sorted, function(el, i) {
return el["ident"] == this.firstFilterMatch;
}.bind(this));
if(idx == null) {
2015-03-27 12:28:16 +00:00
// If no job windows are shown, set up index to make j show the first job window,
// k the last job window.
2015-03-27 14:52:55 +00:00
idx = this.jobs.sorted.length;
2015-03-27 12:11:25 +00:00
}
idx = idx + offset;
2015-03-27 14:52:55 +00:00
// When reaching either end, hide all job windows. When going past
// the end, wrap around.
2015-03-27 12:11:25 +00:00
if(idx == -1) {
2015-03-27 14:52:55 +00:00
idx = this.jobs.sorted.length;
} else if(idx == this.jobs.sorted.length + 1) {
2015-03-27 12:11:25 +00:00
idx = 0;
}
2015-03-27 14:52:55 +00:00
if(idx == this.jobs.sorted.length) {
ds.setFilter("^$");
} else {
var newShownJob = this.jobs.sorted[idx];
ds.setFilter("^" + regExpEscape(newShownJob["url"]) + "$");
}
2015-03-27 12:11:25 +00:00
};
2014-10-09 06:05:49 +00:00
JobsRenderer.prototype.updateAlign = function() {
var adderOrRemover = this.aligned ? classAdder : classRemover;
arrayFrom(document.querySelectorAll('.job-url')).map(adderOrRemover('job-url-aligned'));
arrayFrom(document.querySelectorAll('.job-note')).map(adderOrRemover('job-note-aligned'));
arrayFrom(document.querySelectorAll('.job-nick')).map(adderOrRemover('job-nick-aligned'));
2014-10-09 07:04:46 +00:00
arrayFrom(document.querySelectorAll('.job-mb')).map(adderOrRemover('job-mb-aligned'));
arrayFrom(document.querySelectorAll('.job-responses')).map(adderOrRemover('job-responses-aligned'));
arrayFrom(document.querySelectorAll('.job-in-queue')).map(adderOrRemover('job-in-queue-aligned'));
arrayFrom(document.querySelectorAll('.job-delay')).map(adderOrRemover('job-delay-aligned'));
2014-10-09 06:05:49 +00:00
};
JobsRenderer.prototype.toggleAlign = function() {
this.aligned = !this.aligned;
this.updateAlign();
2014-10-09 05:09:42 +00:00
};
2013-09-23 16:13:38 -05:00
var BatchingQueue = function(callable, minInterval) {
this.callable = callable;
this._minInterval = minInterval;
this.queue = [];
this._timeout = null;
this._boundRunCallable = this._runCallable.bind(this);
};
BatchingQueue.prototype.setMinInterval = function(minInterval) {
this._minInterval = minInterval;
};
BatchingQueue.prototype._runCallable = function() {
this._timeout = null;
var queue = this.queue;
this.queue = [];
this.callable(queue);
};
BatchingQueue.prototype.callNow = function() {
if(this._timeout !== null) {
clearTimeout(this._timeout);
this._timeout = null;
}
this._runCallable();
};
BatchingQueue.prototype.push = function(v) {
this.queue.push(v);
if(this._timeout === null) {
this._timeout = setTimeout(this._boundRunCallable, this._minInterval);
}
};
var Decayer = function(initial, multiplier, max) {
this.initial = initial;
this.multiplier = multiplier;
this.max = max;
this.reset();
};
Decayer.prototype.reset = function() {
// First call to .decay() will multiply, but we want to get the `intitial`
// value on the first call to .decay(), so divide.
this.current = this.initial / this.multiplier;
return this.current;
};
Decayer.prototype.decay = function() {
this.current = Math.min(this.current * this.multiplier, this.max);
return this.current;
};
var Dashboard = function() {
this.messageCount = 0;
var args = getQueryArgs();
var historyLines =
args["historyLines"] ?
Number(args["historyLines"]) :
navigator.userAgent.match(/Mobi/) ? 250 : 1000;
var batchTimeWhenVisible =
args["batchTimeWhenVisible"] ?
Number(args["batchTimeWhenVisible"]) :
125;
var showNicks =
args["showNicks"] ?
2015-03-28 02:34:20 +00:00
Boolean(Number(args["showNicks"])) :
2013-09-23 16:13:38 -05:00
false;
2015-03-28 02:34:20 +00:00
var promo =
args["promo"] ?
Boolean(Number(args["promo"])) :
true;
2014-09-10 06:28:30 +00:00
this.host = args["host"] ? args["host"] : location.host;
2013-09-23 16:13:38 -05:00
this.dumpTraffic = args["dumpMax"] & & Number(args["dumpMax"]) > 0;
if(this.dumpTraffic) {
this.dumpMax = Number(args["dumpMax"]);
}
2014-10-09 07:49:16 +00:00
this.jobsRenderer = new JobsRenderer(
byId('logs'), byId('filter-box'), historyLines, showNicks);
2013-09-23 16:13:38 -05:00
var batchTimeWhenHidden = 5000;
var xhr = new XMLHttpRequest();
xhr.onload = function() {
var recentLines = JSON.parse(xhr.responseText);
for(var i=0; i < recentLines.length ; i + + ) {
this.handleData(recentLines[i]);
}
this.queue = new BatchingQueue(function(queue) {
//console.log("Queue has ", queue.length, "items");
for(var i=0; i < queue.length ; i + + ) {
this.handleData(JSON.parse(queue[i]));
}
}.bind(this), batchTimeWhenVisible);
this.decayer = new Decayer(1000, 1.5, 60000);
this.connectWebSocket();
document.addEventListener("visibilitychange", function() {
if(document.hidden) {
//console.log("Page has become hidden");
this.queue.setMinInterval(batchTimeWhenHidden);
} else {
//console.log("Page has become visible");
this.queue.setMinInterval(batchTimeWhenVisible);
this.queue.callNow();
}
}.bind(this), false);
}.bind(this);
2014-09-10 06:28:30 +00:00
xhr.open("GET", "http://" + this.host + "/logs/recent?cb=" + Date.now() + Math.random());
2014-09-17 23:58:29 -05:00
xhr.setRequestHeader('Accept', 'application/json');
2013-09-23 16:13:38 -05:00
xhr.send("");
2015-03-27 11:28:33 +00:00
document.onkeypress = this.keyPress.bind(this);
2015-03-28 02:34:20 +00:00
if(!promo) {
byId('promo').style.display = 'none';
}
2015-03-27 11:28:33 +00:00
};
Dashboard.prototype.keyPress = function(ev) {
2015-03-27 12:11:25 +00:00
//console.log(ev);
if(ev.which == 106) { // j
this.jobsRenderer.showNextPrev(1);
} else if(ev.which == 107) { // k
this.jobsRenderer.showNextPrev(-1);
2015-03-28 02:28:00 +00:00
} else if(ev.which == 97) { // a
ds.setFilter('');
} else if(ev.which == 110) { // n
ds.setFilter('^$');
} else if(ev.which == 102) { // f
ev.preventDefault();
byId('filter-box').focus();
byId('filter-box').select();
2015-04-09 20:56:36 +00:00
} else if(ev.which == 63) { // ?
ds.toggleHelp();
2015-03-27 11:28:33 +00:00
}
2013-09-23 16:13:38 -05:00
};
Dashboard.prototype.handleData = function(data) {
this.messageCount += 1;
if(this.dumpTraffic & & this.messageCount < = this.dumpMax) {
byId('traffic').appendChild(h("pre", null, prettyJson(data)));
}
this.jobsRenderer.handleData(data);
};
Dashboard.prototype.connectWebSocket = function() {
2014-09-10 06:28:30 +00:00
this.ws = new WebSocket("ws://" + this.host + "/stream");
2013-09-23 16:13:38 -05:00
this.ws.onmessage = function(ev) {
this.queue.push(ev["data"]);
}.bind(this);
this.ws.onopen = function(ev) {
console.log("WebSocket opened:", ev);
this.decayer.reset();
}.bind(this);
this.ws.onclose = function(ev) {
console.log("WebSocket closed:", ev);
var delay = this.decayer.decay();
console.log("Reconnecting in", delay, "ms");
setTimeout(this.connectWebSocket.bind(this), delay);
}.bind(this);
};
2014-10-09 05:32:47 +00:00
Dashboard.prototype.toggleAlign = function() {
2014-10-09 06:05:49 +00:00
this.jobsRenderer.toggleAlign();
2014-10-09 05:32:47 +00:00
};
2013-09-23 16:13:38 -05:00
Dashboard.prototype.toggleHelp = function() {
var help = byId('help');
if(help.classList.contains('undisplayed')) {
help.classList.remove('undisplayed');
} else {
help.classList.add('undisplayed');
}
};
2014-10-09 09:01:13 +00:00
Dashboard.prototype.getFilter = function(value) {
return byId('filter-box').value;
};
Dashboard.prototype.setFilter = function(value) {
byId('filter-box').value = value;
byId('filter-box').onchange();
};
2013-09-23 16:13:38 -05:00
var ds = new Dashboard();
< / script >
< / body >
< / html >