MTurk HIT Database Mk.II

Keep track of the HITs you've done (and more!)

当前为 2015-08-31 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         MTurk HIT Database Mk.II
// @author       feihtality
// @namespace    https://greasyfork.org/en/users/12709
// @version      0.8.082
// @description  Keep track of the HITs you've done (and more!)
// @include      /^https://www\.mturk\.com/mturk/(dash|view|sort|find|prev|search|accept|cont).*/
// @exclude      https://www.mturk.com/mturk/findhits?*hit_scraper
// @grant        none
// ==/UserScript==

/**\
 ** 
 ** This is a complete rewrite of the MTurk HIT Database script from the ground up, which
 ** eliminates obsolete methods, fixes many bugs, and brings this script up-to-date 
 ** with the current modern browser environment.
 **
\**/ 


/*
 * TODO
 *   misc refactoring
 *   optimize searching: index -> date filter
 *   rewrite error handling
 *   tagging (?)
 *   refine searching via R/T buttons
 *   import from old csv format (?)
 *
 */



const DB_VERSION = 2;
const DB_NAME = 'HITDB_TESTING';
const MTURK_BASE = 'https://www.mturk.com/mturk/';
//const TO_BASE = 'http://turkopticon.ucsd.edu/api/multi-attrs.php';

// polyfill for chrome until v45(?) 
if (!NodeList.prototype[Symbol.iterator]) NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
// format leading zeros
Number.prototype.toPadded = function(length) {
  'use strict';

  length = length || 2;
  return ("0000000"+this).substr(-length);
};
// decimal rounding
Math.decRound = function(v, shift) {
  'use strict';

  v = Math.round(+(v+"e"+shift));
  return +(v+"e"+-shift);
};
Date.prototype.toLocalISOString = function() {
  'use strict';

  var pad = function(num) { return Number(num).toPadded(); },
      offset = pad(Math.floor(this.getTimezoneOffset()/60)) + pad(this.getTimezoneOffset()%60),
      timezone = this.getTimezoneOffset() > 0 ? "-" + offset : "+" + offset;
  return this.getFullYear() + "-" + pad(this.getMonth()+1) + "-" + pad(this.getDate()) +
    "T" + pad(this.getHours()) + ":" + pad(this.getMinutes()) + ":" + pad(this.getSeconds()) + timezone;
};

/***********************************************************************************************/

// TODO defer qc init to Promise for case in large obj JSON parsing
var qc = { 
  extraDays: !!localStorage.getItem("hitdb_extraDays") || false, 
  fetchData: document.location.pathname === "/mturk/dashboard" ? JSON.parse(localStorage.getItem("hitdb_fetchData") || "{}") : null,
  seen: {},
  aat: ~document.location.pathname.search(/(dash|accept|cont)/) ? JSON.parse(localStorage.getItem("hitdb_autoAppTemp") || "{}") : null,
  aac:  document.location.pathname === "/mturk/dashboard" ? JSON.parse(localStorage.getItem("hitdb_autoAppCollection") || "{}") : null,
  save: function(key, name, isObj) {
    'use strict';

    if (isObj) 
      localStorage.setItem(name, JSON.stringify(this[key]));
    else 
      localStorage.setItem(name, this[key]);
  }
},
    metrics = {};

var HITStorage = { //{{{
  data: {},

  versionChange: function hsversionChange() { //{{{
    'use strict';

    var db = this.result;
    db.onerror = HITStorage.error;
    db.onversionchange = function(e) { console.log("detected version change??",console.dir(e)); db.close(); };
    this.onsuccess = function() { db.close(); };
    var dbo;

    console.groupCollapsed("HITStorage.versionChange::onupgradeneeded");

    if (!db.objectStoreNames.contains("HIT")) { 
      console.log("creating HIT OS");
      dbo = db.createObjectStore("HIT", { keyPath: "hitId" });
      dbo.createIndex("date", "date", { unique: false });
      dbo.createIndex("requesterName", "requesterName", { unique: false});
      dbo.createIndex("title", "title", { unique: false });
      dbo.createIndex("reward", "reward", { unique: false });
      dbo.createIndex("status", "status", { unique: false });
      dbo.createIndex("requesterId", "requesterId", { unique: false });

      localStorage.setItem("hitdb_extraDays", true);
      qc.extraDays = true;
    }
    
    if (!db.objectStoreNames.contains("STATS")) {
      console.log("creating STATS OS");
      dbo = db.createObjectStore("STATS", { keyPath: "date" });
    }
    if (this.transaction.objectStore("STATS").indexNames.length < 5) { // new in v5: schema additions
      this.transaction.objectStore("STATS").createIndex("approved", "approved", { unique: false });
      this.transaction.objectStore("STATS").createIndex("earnings", "earnings", { unique: false });
      this.transaction.objectStore("STATS").createIndex("pending", "pending", { unique: false });
      this.transaction.objectStore("STATS").createIndex("rejected", "rejected", { unique: false });
      this.transaction.objectStore("STATS").createIndex("submitted", "submitted", { unique: false });
    }

    (function _updateNotes(dbt) { // new in v5: schema change
      if (!db.objectStoreNames.contains("NOTES")) {
        console.log("creating NOTES OS");
        dbo = db.createObjectStore("NOTES", { keyPath: "id", autoIncrement: true });
        dbo.createIndex("hitId", "hitId", { unique: false });
        dbo.createIndex("requesterId", "requesterId", { unique: false });
        dbo.createIndex("tags", "tags", { unique: false, multiEntry: true });
        dbo.createIndex("date", "date", { unique: false });
      }
      if (db.objectStoreNames.contains("NOTES") && dbt.objectStore("NOTES").indexNames.length < 3) {
          _mv(db, dbt, "NOTES", "NOTES", _updateNotes);
      }
    })(this.transaction);

    if (db.objectStoreNames.contains("BLOCKS")) {
      console.log("migrating BLOCKS to NOTES");
      var temp = [];
      this.transaction.objectStore("BLOCKS").openCursor().onsuccess = function() {
        var cursor = this.result;
        if (cursor) {
          temp.push( {
            requesterId: cursor.value.requesterId,
            tags: "Blocked",
            note: "This requester was blocked under the old HitDB. Blocking has been deprecated and removed "+
              "from HIT Databse. All blocks have been converted to a Note."
          } );
          cursor.continue();
        } else {
          console.log("deleting blocks");
          db.deleteObjectStore("BLOCKS");
          for (var entry of temp)
            this.transaction.objectStore("NOTES").add(entry);
        }
      };
    }

    function _mv(db, transaction, source, dest, fn) { //{{{
      var _data = [];
      transaction.objectStore(source).openCursor().onsuccess = function() {
        var cursor = this.result;
          if (cursor) {
            _data.push(cursor.value);
            cursor.continue();
          } else {
            db.deleteObjectStore(source);
            fn(transaction);
            if (_data.length)
              for (var i=0;i<_data.length;i++)
                transaction.objectStore(dest).add(_data[i]);
              //console.dir(_data);
          }
      };
    } //}}}

    console.groupEnd();
  }, // }}} versionChange

  error: function(e) { //{{{
    'use strict';

    if (e === "DatabaseCreationError") {
      var s = document.getElementById("hdbStatusText");
      s.style.color = "red";
      s.innerHTML = "Something went wrong during database creation!<br>Please refresh the page and try again";
      console.log("Writing failed with",e);
      return;
    }
    if (typeof e === "string")
      console.log(e);
    else
      console.log("Encountered",e.target.error.name,"--",e.target.error.message,e);
  }, //}}} onerror

  parseDOM: function(doc) {//{{{
    'use strict';
    
    var statusLabel = document.querySelector("#hdbStatusText");
    statusLabel.style.color = "black";

    var errorCheck = doc.querySelector('td[class="error_title"]');

    if (doc.title.search(/Status$/) > 0)            // status overview
      parseStatus();
    else if (doc.querySelector('td[colspan="4"]'))  // valid status detail, but no data
      parseMisc("next");
    else if (doc.title.search(/Status Detail/) > 0) // status detail with data
      parseDetail();
    else if (errorCheck) {                          // encountered an error page
      // hit max request rate
      if (~errorCheck.textContent.indexOf("page request rate")) {
        var _d = doc.documentURI.match(/\d{8}/)[0],
            _p = doc.documentURI.match(/ber=(\d+)/)[1];
        metrics.dbupdate.mark("[PRE]"+_d+"p"+_p, "start");
        console.log("exceeded max requests; refetching", doc.documentURI);
        statusLabel.innerHTML = "Exceeded maximum server requests; retrying "+Utils.ISODate(_d)+" page "+_p+"."+
          "<br>Please wait...";
        setTimeout(HITStorage.fetch, 550, doc.documentURI);
        return;
      }
      // no more staus details left in range
      else if (qc.extraDays)
        parseMisc("end");
    }
    else 
      throw "ParseError::unhandled document received @"+doc.documentURI;


    function parseStatus() {//{{{
      HITStorage.data = { HIT: [], STATS: [] };
      qc.seen = {};
      ProjectedEarnings.clear();

      var _pastDataExists = Boolean(Object.keys(qc.fetchData).length);
      var raw = { 
        day: doc.querySelectorAll(".statusDateColumnValue"), 
        sub: doc.querySelectorAll(".statusSubmittedColumnValue"),
        app: doc.querySelectorAll(".statusApprovedColumnValue"),
        rej: doc.querySelectorAll(".statusRejectedColumnValue"),
        pen: doc.querySelectorAll(".statusPendingColumnValue"),
        pay: doc.querySelectorAll(".statusEarningsColumnValue") 
      };
      
      var timeout = 0;
      for (var i=0;i<raw.day.length;i++) {
        var d = {};
        var _date = raw.day[i].childNodes[1].href.substr(53);
        d.date      = Utils.ISODate(_date);
        d.submitted = +raw.sub[i].textContent;
        d.approved  = +raw.app[i].textContent;
        d.rejected  = +raw.rej[i].textContent;
        d.pending   = +raw.pen[i].textContent;
        d.earnings  = +raw.pay[i].textContent.substr(1);
        HITStorage.data.STATS.push(d);

        // check whether or not we need to get status detail pages for date, then
        // fetch status detail pages per date in range and slightly slow
        // down GET requests to avoid making too many in too short an interval
        var payload = { encodedDate: _date, pageNumber: 1, sortType: "All" };
        if (_pastDataExists) {
          // date not in range but is new date (or old date but we need updates)
          // lastDate stored in ISO format, fetchData date keys stored in mturk's URI ecnodedDate format
          if ( (d.date > qc.fetchData.lastDate) || ~(Object.keys(qc.fetchData).indexOf(_date)) ) {
            setTimeout(HITStorage.fetch, timeout, MTURK_BASE+"statusdetail", payload);
            timeout += 250;

            qc.fetchData[_date] = { submitted: d.submitted, pending: d.pending };
          } 
        } else { // get everything
          setTimeout(HITStorage.fetch, timeout, MTURK_BASE+"statusdetail", payload);
          timeout += 250;

          qc.fetchData[_date] = { submitted: d.submitted, pending: d.pending };
        }
      } // for
      qc.fetchData.expectedTotal = _calcTotals(qc.fetchData);

      // try for extra days
      if (qc.extraDays === true) {
        localStorage.removeItem("hitdb_extraDays");
        d = _decDate(HITStorage.data.STATS[HITStorage.data.STATS.length-1].date);
        qc.extraDays = d; // repurpose extraDays for QC
        payload = { encodedDate: d, pageNumber: 1, sortType: "All" };
        console.log("fetchrequest for", d, "sent by parseStatus");
        setTimeout(HITStorage.fetch, 1000, MTURK_BASE+"statusdetail", payload);
      }
      qc.fetchData.lastDate = HITStorage.data.STATS[0].date; // most recent date seen

    }//}}} parseStatus

    function parseDetail() {//{{{
      var _date = doc.documentURI.replace(/.+(\d{8}).+/, "$1");
      var _page = doc.documentURI.replace(/.+ber=(\d+).+/, "$1");

      metrics.dbupdate.mark("[PRE]"+_date+"p"+_page, "end");
      console.log("page:", _page, "date:", _date);
      statusLabel.textContent = "Processing "+Utils.ISODate(_date)+" page "+_page;
      var raw = {
        req:      doc.querySelectorAll(".statusdetailRequesterColumnValue"),
        title:    doc.querySelectorAll(".statusdetailTitleColumnValue"),
        pay:      doc.querySelectorAll(".statusdetailAmountColumnValue"),
        status:   doc.querySelectorAll(".statusdetailStatusColumnValue"),
        feedback: doc.querySelectorAll(".statusdetailRequesterFeedbackColumnValue")
      };

      for (var i=0;i<raw.req.length;i++) {
        var d = {};
        d.date          = Utils.ISODate(_date);
        d.feedback      = raw.feedback[i].textContent.trim();
        d.hitId         = raw.req[i].childNodes[1].href.replace(/.+HIT\+(.+)/, "$1");
        d.requesterId   = raw.req[i].childNodes[1].href.replace(/.+rId=(.+?)&.+/, "$1");
        d.requesterName = raw.req[i].textContent.trim().replace(/\|/g,"");
        d.reward        = +raw.pay[i].textContent.substr(1);
        d.status        = raw.status[i].textContent;
        d.title         = raw.title[i].textContent.replace(/\|/g, "");

        // mturk apparently never marks $0.00 HITs as 'Paid' so we fix that
        if (!d.reward && ~d.status.search(/approved/i)) d.status = "Paid";
        // insert autoApproval times
        d.autoAppTime = HITStorage.autoApprovals.getTime(_date,d.hitId);

        HITStorage.data.HIT.push(d);

        if (!qc.seen[_date]) qc.seen[_date] = {};
        qc.seen[_date] = { 
          submitted:   qc.seen[_date].submitted + 1 || 1,
          pending: ~d.status.search(/pending/i)  ? 
            (qc.seen[_date].pending + 1 || 1) : (qc.seen[_date].pending || 0)
        };

        ProjectedEarnings.updateValues(d);
      }

      // additional pages remain; get them
      if (doc.querySelector('img[src="/media/right_dbl_arrow.gif"]')) {
        var payload = { encodedDate: _date, pageNumber: +_page+1, sortType: "All" };
        setTimeout(HITStorage.fetch, 250, MTURK_BASE+"statusdetail", payload);
        return;
      }

      if (!qc.extraDays) { // not fetching extra days
        //no longer any more useful data here, don't need to keep rechecking this date
        if (Utils.ISODate(_date) !== qc.fetchData.lastDate &&
            qc.seen[_date].submitted === qc.fetchData[_date].submitted && 
            qc.seen[_date].pending === 0) {
          console.log("no more pending hits, removing",_date,"from fetchData");
          delete qc.fetchData[_date];
          localStorage.setItem("hitdb_fetchData", JSON.stringify(qc.fetchData));
          HITStorage.autoApprovals.purge(_date);
        }
        // finished scraping; start writing
        console.log("totals", _calcTotals(qc.seen), qc.fetchData.expectedTotal);
        statusLabel.textContent += " [ "+_calcTotals(qc.seen)+"/"+ qc.fetchData.expectedTotal+" ]";
        if (_calcTotals(qc.seen) === qc.fetchData.expectedTotal) {
          statusLabel.textContent = "Writing to database...";
          HITStorage.autoApprovals.purge();
          HITStorage.write(HITStorage.data, "update");
        }
      } else if (_date <= qc.extraDays) { // day is older than default range and still fetching extra days
        parseMisc("next");
        console.log("fetchrequest for", _decDate(Utils.ISODate(_date)));
      }
    }//}}} parseDetail

    function parseMisc(type) {//{{{
      var _d = doc.documentURI.match(/\d{8}/)[0],
          _p = doc.documentURI.match(/ber=(\d+)/)[1];
      metrics.dbupdate.mark("[PRE]"+_d+"p"+_p, "end");
      var payload = { encodedDate: _decDate(Utils.ISODate(_d)), pageNumber: 1, sortType: "All" };

      if (type === "next" && +qc.extraDays > 1) {
        setTimeout(HITStorage.fetch, 250, MTURK_BASE+"statusdetail", payload);
        console.log("going to next page", payload.encodedDate);
      } else if (type === "end" && +qc.extraDays > 1) {
        statusLabel.textContent = "Writing to database...";
        HITStorage.write(HITStorage.data, "update");
      } else 
        throw 'Unhandled case -- "'+type+'" in '+doc.documentURI;
    }//}}}

    function _decDate(date) {//{{{
      var y = date.substr(0,4);
      var m = date.substr(5,2);
      var d = date.substr(8,2);
      date = new Date(y,m-1,d-1);
      return Number(date.getMonth()+1).toPadded() + Number(date.getDate()).toPadded() + date.getFullYear();
    }//}}}

    function _calcTotals(obj) {//{{{
      var sum = 0;
      for (var k in obj){
        if (obj.hasOwnProperty(k) && !isNaN(+k)) 
          sum += obj[k].submitted;
      }
      return sum;
    }//}}}
  },//}}} parseDOM
  
  autoApprovals: {//{{{
    getTime : function(date, hitId) {
      'use strict';

      if (qc.extraDays || (!Object.keys(qc.aac).length && !Object.keys(qc.aat).length)) return "";
      var found = false,
          filter = function(id) { return id === hitId; },
          autoApp = "";

      if (qc.aac[date]) {
        autoApp = qc.aac[date][Object.keys(qc.aac[date]).filter(filter)[0]] || "";
        if (autoApp) found = true;
      }
      if (!found && Object.keys(qc.aat).length) {
        for (var key in qc.aat) { if (qc.aat.hasOwnProperty(key)) { // for all dates in aat
          var id = Object.keys(qc.aat[key]).filter(filter)[0];
          autoApp = qc.aat[key][id] || "";
          if (autoApp) {
            found = true;
            qc.aac[date] = qc.aac[date] || {};
            qc.aac[date][id] = qc.aat[key][id]; // move time from temp var to collection var
            delete qc.aat[key][id];
            qc.save("aat", "hitdb_autoAppTemp", true);
            qc.save("aac", "hitdb_autoAppCollection", true);
            break;
          }
        }} // for key (dates)
      } // if !found && aat not empty
      return autoApp;
    },// getTime
    purge : function(date) {
      'use strict';
      if (date) { 
        delete qc.aac[date];
        qc.save("aac", "hitdb_autoAppCollection", true);
        return;
      }

      if (!Object.keys(qc.aat).length) return; // nothing here

      var pad = function(num) { return Number(num).toPadded(); },
          _date = Date.parse(new Date().getFullYear() + "-" + pad(new Date().getMonth()+1) + "-" + pad(new Date().getDate()));

      for (var key of Object.keys(qc.aat)) {
        if (_date - key > 169200000) delete qc.aat[key]; // at least 2 days old, no need to keep it around
      }
      qc.save("aat", "hitdb_autoAppTemp", true);
    } // purge
  },//}}} autoApprovals

  fetch: function(url, payload) { //{{{
    'use strict';

    //format GET request with query payload
    if (payload) {
      var args = 0;
      url += "?";
      for (var k in payload) {
        if (payload.hasOwnProperty(k)) {
          if (args++) url += "&";
          url += k + "=" + payload[k];
        }
      }
    }
    // defer XHR to a promise
    var fetch = new Promise( function(fulfill, deny) {
      var urlreq = new XMLHttpRequest();
      urlreq.open("GET", url, true);
      urlreq.responseType = "document";
      urlreq.send();
      urlreq.onload = function() { 
        if (this.status === 200) {
          fulfill(this.response);
        } else {
          deny("Error ".concat(String(this.status)).concat(": "+this.statusText));
        }
      };
      urlreq.onerror   = function() { deny("Error ".concat(String(this.status)).concat(": "+this.statusText)); };
      urlreq.ontimeout = function() { deny("Error ".concat(String(this.status)).concat(": "+this.statusText)); };
    } );
    fetch.then( HITStorage.parseDOM, HITStorage.error );

  }, //}}} fetch
  
  write: function(input, statusUpdate) { //{{{
    'use strict';

    if (statusUpdate === "update")
      qc.timeoutTimer = setTimeout(HITStorage.error, 5555, "DatabaseCreationError");

    var dbh = window.indexedDB.open(DB_NAME);
    dbh.onerror = HITStorage.error;
    dbh.onsuccess = function() { _write(this.result); };

    var counts = { requests: 0, total: 0 };

    function _write(db) {
      db.onerror = HITStorage.error;
      var os = Object.keys(input);

      var dbt = db.transaction(os, "readwrite");
      var dbo = [];
      for (var i=0;i<os.length;i++) { // cycle object stores
        dbo[i] = dbt.objectStore(os[i]);
        for (var k of input[os[i]]) { // cycle entries to put into object stores
          if (statusUpdate && ++counts.requests)
            dbo[i].put(k).onsuccess = _statusCallback;
          else
            dbo[i].put(k);
        }
      }
      db.close();
    }

    function _statusCallback() {
      if (++counts.total === counts.requests) {
        var statusLabel = document.querySelector("#hdbStatusText");
        statusLabel.style.color = "green";
        statusLabel.textContent = statusUpdate === "update" ? "Update Complete!" : 
          statusUpdate === "restore" ? "Restoring " + counts.total + " entries... Done!" : 
          "Done!";
        document.querySelector("#hdbProgressBar").style.display = "none";

        if (statusUpdate === "update") {
          clearTimeout(qc.timeoutTimer);
          ProjectedEarnings.data.dbUpdated = new Date().toLocalISOString();
          ProjectedEarnings.saveState();
          ProjectedEarnings.draw(false);

          metrics.dbupdate.stop();
          metrics.dbupdate.report();
        }
      }
    }

  }, //}}} write

  recall: function(store, options) {//{{{
    'use strict';

    var index = options ? (options.index  || null)  : null,
        range = options ? (options.range  || null)  : null,
        dir   = options ? (options.dir || "next") : "next",
        fs    = options ? (options.filter ? options.filter.status !== "*" ? new RegExp(options.filter.status, "i") : false : false) : false,
        fq    = options ? (options.filter ? options.filter.query  !== "*" ? new RegExp(options.filter.query,"i")  : false : false) : false,
        limit = 0;

    if (options && options.progress) {
      var progressBar = document.querySelector("#hdbProgressBar");
          //statusText  = document.querySelector("#hdbStatusText");
      progressBar.style.display = "block";
    }
    var sr = new DatabaseResult();
    return new Promise( function(resolve) {
      window.indexedDB.open(DB_NAME).onsuccess = function() {
        var dbo = this.result.transaction(store, "readonly").objectStore(store), dbq = null;
        if (index) 
          dbq = dbo.index(index).openCursor(range, dir);
        else
          dbq = dbo.openCursor(range, dir);
        dbq.onsuccess = function() {
          var c = this.result;
          if (c) { 
            if ( (!fs && !fq) ||                              // no query filter and no status filter OR
                 (fs && !fq && ~c.value.status.search(fs)) || // status match and no query filter OR
                 (!fs && fq &&                                // query match and no status filter OR
                   (~c.value.title.search(fq) || ~c.value.requesterName.search(fq) || ~c.value.hitId.search(fq)))  ||
                 (fs && fq && ~c.value.status.search(fs) &&   // status match and query match
                   (~c.value.title.search(fq) || ~c.value.requesterName.search(fq) || ~c.value.hitId.search(fq))) )
              if (limit++ < 3800) // limit to save memory usage in large databases
                sr.include(c.value);
            c.continue();
          } else
            resolve(sr);
        };
      };
    } ); // promise
  },//}}} recall

  backup: function() {//{{{
    'use strict';

    var bData = {},
        os    = ["STATS", "NOTES", "HIT"],
        count = 0,
        prog  = document.querySelector("#hdbProgressBar");

    prog.style.display = "block";

    window.indexedDB.open(DB_NAME).onsuccess = function() {
      for (var store of os) {
        this.result.transaction(os, "readonly").objectStore(store).openCursor().onsuccess = populateBackup;
      }
    };
    function populateBackup(e) {
      var cursor = e.target.result;
      if (cursor) {
        if (!bData[cursor.source.name]) bData[cursor.source.name] = [];
        bData[cursor.source.name].push(cursor.value);
        cursor.continue();
      } else 
        if (++count === 3)
          finalizeBackup();
    }
    function finalizeBackup() {
      var backupblob = new Blob([JSON.stringify(bData)], {type:"application/json"});
      var date = new Date();
      var dl = document.createElement("A");
      date = date.getFullYear() + Number(date.getMonth()+1).toPadded() + Number(date.getDate()).toPadded();
      dl.href = URL.createObjectURL(backupblob);
      console.log(dl.href);
      dl.download = "hitdb_"+date+".bak";
      document.body.appendChild(dl); // FF doesn't support forced events unless element is part of the document
      dl.click();                    // so we make it so and click,
      dl.remove();                   // then immediately remove it
      prog.style.display = "none";
    }

  }//}}} backup

}, //}}} HITStorage

    Utils = { //{{{
  ftime : function(t, noBlanks) {//{{{
    'use strict';

    if (t === 0) return "0s";
    if (!t && noBlanks) return "n/a";
    var d = Math.floor(t/86400),
        h = Math.floor(t%86400/3600),
        m = Math.floor(t%86400%3600/60),
        s = t%86400%3600%60;
    return ((d>0) ? d+" day"+(d>1 ? "s " : " ") : "") + ((h>0) ? h+"h " : "") + ((m>0) ? m+"m " : "") + ((s>0) ? s+"s" : "");
  },//}}}ftime

  ISODate: function(date) { //{{{ MMDDYYYY <-> YYYY-MM-DD
    'use strict';

    if (date.length === 10)
      return date.substr(5,2)+date.substr(-2)+date.substr(0,4);
    else
      return date.substr(4)+"-"+date.substr(0,2)+"-"+date.substr(2,2);
  }//}}} ISODate

}; //}}} Utils

// ProjectedEarnings doesn't belong up here, but it needs to be for variable assignment purposes :(
var ProjectedEarnings = {//{{{
  data: JSON.parse(localStorage.getItem("hitdb_projectedEarnings") || "{}"),
  updateDate: function() {//{{{
    'use strict';

    var el        = document.querySelectorAll(".metrics-table")[5].querySelector(".metrics-table-first-value").children[0],
        date      = el.href.match(/\d{8}/)[0],
        day       = el.textContent,
        isToday   = day === "Today",
        _date     = new Date(),
        pad       = function(num) { return Number(num).toPadded(); },
        weekEnd   = null,
        weekStart = null;

    _date.setDate(_date.getDate() - _date.getDay()); // sunday
    weekStart = Date.parse(_date.getFullYear() + "-" + pad(_date.getMonth()+1) + "-" + pad(_date.getDate()));
    _date.setDate(_date.getDate() + 7); // next sunday
    weekEnd   = Date.parse(_date.getFullYear() + "-" + pad(_date.getMonth()+1) + "-" + pad(_date.getDate()-_date.getDay()+7));

    if (!Object.keys(this.data).length) {
      this.data = {
        today: date, weekStart: weekStart, weekEnd: weekEnd, day: _date.getDay(), dbUpdated: "n/a",
        pending: 0, earnings: { day: 0, week: 0 }, target: { day: 0, week: 0 }
      };
    }

    if ( (Date.parse(Utils.ISODate(date)) >= this.data.weekEnd) ||
        (!isToday && _date.getDay() < this.data.day) ) { // new week
      this.data.earnings.week = 0;
      this.data.weekEnd = weekEnd;
      this.data.weekStart = weekStart;
    }
    if (date !== this.data.today || !isToday) { // new day
      this.data.today = date;
      this.data.day = _date.getDay();
      this.data.earnings.day = 0;
    }

    this.saveState();
  },//}}} updateDate
  
  draw: function(init) {//{{{
    'use strict';

    var parentTable = document.querySelector("#total_earnings_amount").offsetParent,
        rowPending  = init ? parentTable.insertRow(-1) : parentTable.rows[4],
        rowProjectedDay = init ? parentTable.insertRow(-1) : parentTable.rows[5],
        rowProjectedWeek = init ? parentTable.insertRow(-1) : parentTable.rows[6],
        title = "Click to set/change the target value";

    if (init) {
      rowPending.insertCell(-1);rowPending.insertCell(-1);rowPending.className = "even";
      rowProjectedDay.insertCell(-1);rowProjectedDay.insertCell(-1);rowProjectedDay.className = "odd";
      rowProjectedWeek.insertCell(-1);rowProjectedWeek.insertCell(-1);rowProjectedWeek.className = "even";
      for (var i=0;i<rowPending.cells.length;i++) rowPending.cells[i].style.borderTop = "dotted 1px black";
      rowPending.cells[0].className = "metrics-table-first-value";
      rowProjectedDay.cells[0].className = "metrics-table-first-value";
      rowProjectedWeek.cells[0].className = "metrics-table-first-value";
      rowPending.cells[1].title = "This value includes all earnings that are not yet fully cleared as 'Paid'";
    }

    rowPending.cells[0].innerHTML = 'Pending earnings '+
      '<span style="font-family:arial;font-size:10px;" title="Timestamp of last database update">[ ' + this.data.dbUpdated + ' ]</span>';
    rowPending.cells[1].textContent = "$"+Number(this.data.pending).toFixed(2);
    rowProjectedDay.cells[0].innerHTML = 'Projected earnings for the day<br>'+
      '<meter id="projectedDayProgress" style="width:220px;" title="'+title+
        '" value="'+this.data.earnings.day+'" max="'+this.data.target.day+'"></meter>'+
      '<span style="color:blue;font-family:arial;font-size:10px;"> ' + Number(this.data.earnings.day-this.data.target.day).toFixed(2) + '</span>';
    rowProjectedDay.cells[1].textContent = "$"+Number(this.data.earnings.day).toFixed(2);
    rowProjectedWeek.cells[0].innerHTML = 'Projected earnings for the week<br>' +
      '<meter id="projectedWeekProgress" style="width:220px;" title="'+title+
        '" value="'+this.data.earnings.week+'" max="'+this.data.target.week+'"></meter>' +
      '<span style="color:blue;font-family:arial;font-size:10px;"> ' + Number(this.data.earnings.week-this.data.target.week).toFixed(2) + '</span>';
    rowProjectedWeek.cells[1].textContent = "$"+Number(this.data.earnings.week).toFixed(2);

    document.querySelector("#projectedDayProgress").onclick = updateTargets.bind(this, "day");
    document.querySelector("#projectedWeekProgress").onclick = updateTargets.bind(this, "week");

    function updateTargets(span, e) {
      /*jshint validthis:true*/
      var goal = prompt("Set your " + (span === "day" ? "daily" : "weekly") + " target:",
          this.data.target[span === "day" ? "day" : "week"]);
      if (goal && !isNaN(goal)) {
        this.data.target[span === "day" ? "day" : "week"] = goal;
        e.target.max = goal;
        e.target.nextSibling.textContent = Number(this.data.earnings[span==="day" ? "day":"week"] - goal).toFixed(2);
        this.saveState();
      }
    }
  },//}}} draw
  
  saveState: function() {
    'use strict';

    localStorage.setItem("hitdb_projectedEarnings", JSON.stringify(this.data));
  },

  clear: function() {
    'use strict';

    this.data.earnings = { day:0, week:0 };
    this.data.pending = 0;
  },

  updateValues: function(obj) {
    'use strict';

    var vDate = Date.parse(obj.date);

    if (~obj.status.search(/pending/i)) // sum pending earnings (include approved until fully cleared as paid)
      this.data.pending = Math.decRound(obj.reward+this.data.pending, 2);
    if (Utils.ISODate(obj.date) === this.data.today && !~obj.status.search(/rejected/i)) // sum daily earnings
      this.data.earnings.day = Math.decRound(obj.reward+this.data.earnings.day, 2);
    if (vDate < this.data.weekEnd && vDate >= this.data.weekStart && !~obj.status.search(/rejected/i)) // sum weekly earnings
      this.data.earnings.week = Math.decRound(obj.reward+this.data.earnings.week, 2);
  }
};//}}} ProjectedEarnings

function DatabaseResult() {//{{{
  'use strict';

  this.results = [];
  this.formatHTML = function(type, simple) {//{{{
    simple = simple || false;
    var count = 0, htmlTxt = [], entry = null, _trClass = null;

    if (this.results.length < 1) return "<h2>No entries found matching your query.</h2>";

    if (type === "daily") {
      htmlTxt.push('<thead><tr class="hdbHeaderRow"><th style="background:white;"></th>'+
          '<th>Date</th><th>Submitted</th><th>Approved</th><th>Rejected</th><th>Pending</th><th>Earnings</th></tr></thead><tbody>');
      var r = _collate(this.results,"date");
      for (entry of this.results) {
        _trClass = (count++ % 2 === 0) ? 'class="even"' : 'class="odd"';
        
        htmlTxt.push('<tr '+_trClass+' style="text-align:center"><td style="background:white;"></td>'+
            '<td>' + entry.date + '</td><td>' + entry.submitted + '</td>' +
            '<td>' + entry.approved + '</td><td>' + entry.rejected + '</td><td>' + entry.pending + '</td>' +
            '<td>' + Number(entry.earnings).toFixed(2) + '</td></tr>');
      }
      htmlTxt.push('</tbody><tfoot><tr class="hdbTotalsRow"><td style="text-align:right;">Totals:</td>' +
          '<td style="text-align:right;">' + r.totalEntries + ' days</td><td style="text-align:center;">' + r.totalSub + '</td>' +
          '<td style="text-align:center;">' + r.totalApp + '</td><td style="text-align:center;">' + r.totalRej + '</td>' +
          '<td style="text-align:center;">' + r.totalPen + '</td><td style="text-align:center;">$' + 
          Number(Math.decRound(r.totalPay,2)).toFixed(2) + '</td></tr></tfoot>');
    } else if (type === "pending" || type === "requester") {
      htmlTxt.push('<thead><tr data-sort="99999" class="hdbHeaderRow"><th>Requester ID</th>' +
          '<th width="500">Requester</th><th>' + (type === "pending" ? 'Pending' : 'HITs') + '</th><th>Rewards</th></tr></thead><tbody>');
      r = _collate(this.results,"requesterId");
      for (var k in r) {
        if (!~k.search(/total/) && r.hasOwnProperty(k)) {
          var tr = ['<tr data-hits="'+r[k].length+'"><td>' +
              '<span style="cursor:pointer;color:blue;" class="hdbExpandRow" title="Display all pending HITs from this requester">' +
              '[+]</span> ' + r[k][0].requesterId + '</td><td>' + r[k][0].requesterName + '</td>' +
              '<td style="text-align:center;">' + r[k].length + '</td><td>' + Number(Math.decRound(r[k].pay,2)).toFixed(2) + '</td></tr>'];

          for (var hit of r[k]) {  // hits in range per requester id
            tr.push('<tr data-rid="'+r[k][0].requesterId+'" style="color:#c60000;display:none;"><td style="text-align:right">' + 
                hit.date + '</td><td width="500" colspan="2">[ <span class="helpSpan" title="Auto-approval time">AA: '+
                Utils.ftime(hit.autoAppTime, true).trim()+'</span> ] '+ 
                hit.title + '</td><td style="text-align:right">' + _parseRewards(hit.reward,"pay") + '</td></tr>');
          }
          htmlTxt.push(tr.join(''));
        }
      }
      htmlTxt.sort(function(a,b) { return +b.substr(15,5).match(/\d+/) - +a.substr(15,5).match(/\d+/); });
      htmlTxt.push('</tbody><tfoot><tr class="hdbTotalsRow"><td style="text-align:right;">Totals:</td>' +
          '<td style="text-align:center;">' + (Object.keys(r).length-7) + ' Requesters</td>' +
          '<td style="text-align:right;">' + r.totalEntries + '</td>'+
          '<td style="text-align:right;">$' + Number(Math.decRound(r.totalPay,2)).toFixed(2) + '</td></tr></tfoot>');
    } else { // default
      if (!simple)
        htmlTxt.push('<thead><tr class="hdbHeaderRow"><th colspan="3"></th>' +
          '<th colspan="2" title="Bonuses must be added in manually.\n\nClick inside' +
          'the cell to edit, click out of the cell to save">Reward</th><th colspan="3"></th></tr>'+
          '<tr class="hdbHeaderRow">' +
          '<th>Date</th><th>Requester</th><th>HIT title</th><th style="font-size:10px;">Pay</th>'+
          '<th style="font-size:10px;"><span class="helpSpan" title="Click the cell to edit.\nIts value is automatically saved">'+
          'Bonus</span></th><th>Status</th><th>'+
          '<span class="helpSpan" title="Auto-approval times">AA</span></th><th>Feedback</th></tr></thead><tbody>');

      for (entry of this.results) {
        _trClass = (count++ % 2 === 0) ? 'class="even"' : 'class="odd"';
        var _stColor = ~entry.status.search(/(paid|approved)/i) ? "green"  :
                       entry.status === "Pending Approval"      ? "orange" : "red";
        var href = MTURK_BASE+'contact?requesterId='+entry.requesterId+'&requesterName='+entry.requesterName+
          '&subject=Regarding+Amazon+Mechanical+Turk+HIT+'+entry.hitId;

        if (!simple)
          htmlTxt.push('<tr '+_trClass+' data-id="'+entry.hitId+'">'+
            '<td width="74px">' + entry.date + '</td><td style="max-width:145px;">' +
            '<a target="_blank" title="Contact this requester" href="'+href+'">' + entry.requesterName + '</a></td>' + 
            '<td width="375px" title="HIT ID:   '+entry.hitId+'">' + 
            '<span title="Add a note" id="note-'+entry.hitId+'" style="cursor:pointer;">&nbsp;&#128221;&nbsp;</span>' + 
            entry.title + '</td><td style="text-align:right">' + _parseRewards(entry.reward,"pay") + '</td>' +
            '<td style="text-align:right" class="bonusCell" title="Click to add/edit" contenteditable="true" data-hitid="'+entry.hitId+'">' + 
            (+_parseRewards(entry.reward,"bonus") ? _parseRewards(entry.reward,"bonus") : "") + 
            '</td><td style="color:'+_stColor+';text-align:center">' + entry.status + '</td>' +
            '<td>' + Utils.ftime(entry.autoAppTime) + '</td><td>' + entry.feedback + '</td></tr>');
        else
          htmlTxt.push('<tr data-rid="'+entry.requesterId+'" style="display:none"><td>'+entry.date+'</td><td>'+entry.title+'</td><td>'+
            _parseRewards(entry.reward,"pay") + '</td><td>'+ entry.status+'</td></tr>');
      }

      if (!simple) {
        r = _collate(this.results,"requesterId");
        htmlTxt.push('</tbody><tfoot><tr class="hdbTotalsRow"><td></td>' +
            '<td style="text-align:right">Totals:</td><td style="text-align:center;">' + r.totalEntries + ' HITs</td>' +
            '<td style="text-align:right">$' + Number(Math.decRound(r.totalPay,2)).toFixed(2) + '</td>' +
            '<td style="text-align:right">$' + Number(Math.decRound(r.totalBonus,2)).toFixed(2) + '</td>' +
            '<td colspan="3"></td></tr></tfoot>');
      }
    }
    return htmlTxt.join('');
  };//}}} formatHTML
  this.formatCSV = function(type) {//{{{
    var csvTxt = [], entry = null, delimiter="\t";
    if (type === "daily") {
      csvTxt.push( ["Date", "Submitted", "Approved", "Rejected", "Pending", "Earnings\n"].join(delimiter) );
      for (entry of this.results) {
        csvTxt.push( [entry.date, entry.submitted, entry.approved, entry.rejected, 
            entry.pending, Number(entry.earnings).toFixed(2)+"\n"].join(delimiter) );
      }
      csvToFile(csvTxt, "hitdb_dailyOverview.csv");
    } else if (type === "pending" || type === "requester") {
      csvTxt.push( ["RequesterId","Requester", (type === "pending" ? "Pending" : "HITs"), "Rewards\n"].join(delimiter) );
      var r = _collate(this.results,"requesterId");
      for (var k in r) {
        if (!~k.search(/total/) && r.hasOwnProperty(k))
          csvTxt.push( [k, r[k][0].requesterName, r[k].length, Number(Math.decRound(r[k].pay,2)).toFixed(2)+"\n"].join(delimiter) );
      }
      csvToFile(csvTxt, "hitdb_"+type+"Overview.csv");
    } else {
      csvTxt.push(["Date","Requester","Title","Pay","Bonus","Status","Feedback\n"].join(delimiter));
      for (entry of this.results) {
        csvTxt.push([entry.date, entry.requesterName, entry.title, Number(_parseRewards(entry.reward,"pay")).toFixed(2),
            (+_parseRewards(entry.reward,"bonus") ? Number(_parseRewards(entry.reward,"bonus")).toFixed(2) : ""),
            entry.status, entry.feedback+"\n"].join(delimiter));
      }
      csvToFile(csvTxt, "hitdb_queryResults.csv");
    }

    return "<pre>"+csvTxt.join('')+"</pre>";

    function csvToFile(csv, filename) {
      var blob = new Blob(csv, {type: "text/csv", endings: "native"}),
          dl   = document.createElement("A");
      dl.href = URL.createObjectURL(blob);
      dl.download = filename;
      document.body.appendChild(dl); // FF doesn't support forced events unless element is part of the document
      dl.click();                    // so we make it so and click,
      dl.remove();                   // then immediately remove it
      return dl;
    }
  };//}}} formatCSV
  this.include = function(value) {
    this.results.push(value);
  };

  function _parseRewards(rewards,value) {
    if (!isNaN(rewards)) {
      if (value === "pay")
        return Number(rewards).toFixed(2);
      else
        return "0.00";
    } else {
      if (value === "pay")
        return Number(rewards.pay).toFixed(2);
      else
        return Number(rewards.bonus).toFixed(2);
    }
  } // _parse
  function _collate(data, index) {
    var r = { 
      totalPay: 0, totalBonus: 0, totalEntries: data.length,
      totalSub: 0, totalApp: 0, totalRej: 0, totalPen: 0
    };
    for (var e of data) {
      if (!r[e[index]]) { r[e[index]] = []; r[e[index]].pay = 0; }
      r[e[index]].push(e);

      if (index === "date") {
        r.totalSub += e.submitted;
        r.totalApp += e.approved;
        r.totalRej += e.rejected;
        r.totalPen += e.pending;
        r.totalPay += e.earnings;
      } else {
        r[e[index]].pay += (+_parseRewards(e.reward,"pay"));
        r.totalPay += (+_parseRewards(e.reward,"pay"));
        r.totalBonus += (+_parseRewards(e.reward,"bonus"));
      }
    }
    return r;
  } // _collate
}//}}} databaseresult

/* 
 *
 *    Above contains the core functions. Below is the
 *    main body, interface, and tangential functions.
 *
 *///{{{
// the Set() constructor is never actually used other than to test for Chrome v38+
if (!("indexedDB" in window && "Set" in window)) alert("HITDB::Your browser is too outdated or otherwise incompatible with this script!");
else {
/*
  var tdbh = window.indexedDB.open(DB_NAME);
  tdbh.onerror = function(e) { 'use strict'; console.log("[TESTDB]",e.target.error.name+":", e.target.error.message, e); };
  tdbh.onsuccess = INFLATEDUMMYVALUES;
  tdbh.onupgradeneeded = BLANKSLATE;
  var dbh = null;
*/
  
  if (document.location.pathname.search(/dashboard/) > 0) {
    var dbh = window.indexedDB.open(DB_NAME, DB_VERSION);
    dbh.onerror = function(e) { 'use strict'; console.log("[HITDB]",e.target.error.name+":", e.target.error.message, e); };
    dbh.onupgradeneeded = HITStorage.versionChange;
    dbh.onsuccess = function() { 'use strict'; this.result.close(); };

    dashboardUI();

    ProjectedEarnings.updateDate();
    ProjectedEarnings.draw(true);
  } else {
    beenThereDoneThat();
  }
}
/*}}}
 *
 *    Above is the main body and core functions. Below
 *    defines UI layout/appearance and tangential functions.
 *
 */

// {{{ css injection
var css = "<style type='text/css'>" +
".hitdbRTButtons {border:1px solid; font-size: 10px; height: 18px; padding-left: 5px; padding-right: 5px; background: pink;}" +
".hitdbRTButtons-green {background: lightgreen;}" +
".hitdbRTButtons-large {width:80px;}" +
".hdbProgressContainer {margin:auto; width:500px; height:6px; position:relative; display:none; border-radius:10px; overflow:hidden; background:#d3d8db;}" +
".hdbProgressInner {width:100%; position:absolute; left:0;top:0;bottom:0; animation: kfpin 1.4s infinite; background:" +
  "linear-gradient(262deg, rgba(208,69,247,0), rgba(208,69,247,1), rgba(69,197,247,1), rgba(69,197,247,0)); background-size: 300% 500%;}" +
".hdbProgressOuter {width:30%; position:absolute; left:0;top:0;bottom:0; animation: kfpout 2s cubic-bezier(0,0.55,0.2,1) infinite;}" +
"@keyframes kfpout { 0% {left:-100%;} 70%{left:100%;} 100%{left:100%;} }" +
"@keyframes kfpin { 0%{background-position: 0% 50%} 50%{background-position: 100% 15%} 100%{background-position:0% 30%} }" +
".hdbCalControls {cursor:pointer;} .hdbCalControls:hover {color:c27fcf;}" +
".hdbCalCells {background:#f0f6f9; height:19px}" +
".hdbCalDays {cursor:pointer; text-align:center;} .hdbCalDays:hover {background:#7fb4cf; color:white;}" +
".hdbDayHeader {width:26px; text-align:center; font-weight:bold; font-size:12px; background:#f0f6f9;}" +
".hdbCalHeader {background:#7fb4cf; color:white; font-weight:bold; text-align:center; font-size:11px; padding:3px 0px;}" +
"#hdbCalendarPanel {position:absolute; z-index:10; box-shadow:-2px 3px 5px 0px rgba(0,0,0,0.68);}" +
".hdbTotalsRow {background:#CCC; color:#369; font-weight:bold;}" +
".hdbHeaderRow {background:#7FB448; font-size:12px; color:white}" +
".helpSpan {border-bottom:1px dotted; cursor:help;}" +
"</style>";
document.head.innerHTML += css;
// }}}

function beenThereDoneThat() {//{{{
  // 
  // TODO refine searching
  //
  'use strict';

  if (~document.location.pathname.search(/(accept|continue)/)) {
    if (!document.querySelector('input[name="hitAutoAppDelayInSeconds"]')) return;

    // capture autoapproval times
    var _aa = document.querySelector('input[name="hitAutoAppDelayInSeconds"]').value,
        _hid = document.querySelectorAll('input[name="hitId"]')[1].value,
        pad = function(num) { return Number(num).toPadded(); },
        _d  = Date.parse(new Date().getFullYear() + "-" + pad(new Date().getMonth()+1) + "-" + pad(new Date().getDate()));

    if (!qc.aat[_d]) qc.aat[_d] = {};
    qc.aat[_d][_hid] = _aa;
    qc.save("aat", "hitdb_autoAppTemp", true);
    return;
  }
  var qualNode = document.querySelector('td[colspan="11"]');
  if (qualNode) { // we're on the preview page!
    var requester     = document.querySelector('input[name="requesterId"]').value,
        //hitId         = document.querySelector('input[name="hitId"]').value,
        autoApproval  = document.querySelector('input[name="hitAutoAppDelayInSeconds"]').value,
        hitTitle      = document.querySelector('div[style*="ellipsis"]').textContent.trim().replace(/\|/g,""),
        insertionNode = qualNode.parentNode.parentNode;
    var row = document.createElement("TR"), cellL = document.createElement("TD"), cellR = document.createElement("TD");
    var _resultsTable = document.createElement("TABLE");
    _resultsTable.id = "resultsTableFor"+requester;
    insertionNode.parentNode.parentNode.appendChild(_resultsTable);

    cellR.innerHTML = '<span class="capsule_field_title">Auto-Approval:</span>&nbsp;&nbsp;'+Utils.ftime(autoApproval);
    var rbutton = document.createElement("BUTTON");
    rbutton.classList.add("hitdbRTButtons","hitdbRTButtons-large");
    rbutton.textContent = "Requester";
    rbutton.onclick = function(e) { 
      e.preventDefault();
      showResults(requester);
    };
    var tbutton = rbutton.cloneNode(false);
    rbutton.dataset.id = requester;
    rbutton.title = "Show HITs completed from this requester";
    tbutton.textContent = "HIT Title";
    tbutton.onclick = function(e) { e.preventDefault(); };
    HITStorage.recall("HIT", {index: "requesterId", range: window.IDBKeyRange.only(requester)})
      .then(processResults.bind(rbutton));
    HITStorage.recall("HIT", {index: "title", range: window.IDBKeyRange.only(hitTitle)})
      .then(processResults.bind(tbutton));
    row.appendChild(cellL);
    row.appendChild(cellR);
    cellL.appendChild(rbutton);
    cellL.appendChild(tbutton);
    cellL.colSpan = "3";
    cellR.colSpan = "8";
    insertionNode.appendChild(row);
  } else { // browsing HITs n sutff 
    var titleNodes = document.querySelectorAll('a[class="capsulelink"]');
    if (titleNodes.length < 1) return; // nothing left to do here!
    var requesterNodes = document.querySelectorAll('a[href*="hitgroups&requester"]');
    var insertionNodes = [];

    for (var i=0;i<titleNodes.length;i++) {
      var _title = titleNodes[i].textContent.trim().replace(/\|/g,"");
      var _tbutton = document.createElement("BUTTON");
      var _id = requesterNodes[i].href.replace(/.+Id=(.+)/, "$1");
      var _rbutton = document.createElement("BUTTON");
      var _div = document.createElement("DIV"), _tr = document.createElement("TR");
      _resultsTable = document.createElement("TABLE");
      insertionNodes.push(requesterNodes[i].parentNode.parentNode.parentNode);
      insertionNodes[i].offsetParent.offsetParent.offsetParent.offsetParent.appendChild(_resultsTable);
      _resultsTable.id = "resultsTableFor"+_id;

      HITStorage.recall("HIT", {index: "title", range: window.IDBKeyRange.only(_title)} )
        .then(processResults.bind(_tbutton));
      HITStorage.recall("HIT", {index: "requesterId", range: window.IDBKeyRange.only(_id)} )
        .then(processResults.bind(_rbutton));

      _tr.appendChild(_div);
      _div.id = "hitdbRTInjection-"+i;
      _div.appendChild(_rbutton);
      _rbutton.textContent = 'R';
      _rbutton.classList.add("hitdbRTButtons");
      _rbutton.dataset.id = _id;
      _rbutton.onclick = showResults.bind(null, _id, null);
      _rbutton.title = "Show HITs completed from this requester";
      _div.appendChild(_tbutton);
      _tbutton.textContent = 'T';
      _tbutton.classList.add("hitdbRTButtons");
      insertionNodes[i].appendChild(_tr);
    }
  } // else

  function showResults(rid, title) {
    console.log(rid, title);
    var el = null;
    if (rid) {
      for (el of document.querySelectorAll('tr[data-rid="'+rid+'"]')) {
        if (el.style.display === "none")
          el.style.display = "table-row";
        else
          el.style.display = "none";
      }
    }
  }

  function processResults(r) {
    /*jshint validthis: true*/
    if (r.results.length) {
      this.classList.add("hitdbRTButtons-green");
      if (this.dataset.id) {
        var rtable = document.querySelector("#resultsTableFor"+this.dataset.id);
        rtable.innerHTML += r.formatHTML(null,true);
      }
    }
  }

  
}//}}} btdt

function dashboardUI() {//{{{
  //
  // TODO refactor
  //
  'use strict';

  var controlPanel = document.createElement("TABLE");
  var insertionNode = document.querySelector(".footer_separator").previousSibling;
  document.body.insertBefore(controlPanel, insertionNode);
  controlPanel.width = "760";
  controlPanel.align = "center";
  controlPanel.cellSpacing = "0";
  controlPanel.cellPadding = "0";
  controlPanel.innerHTML = '<tr height="25px"><td width="10" bgcolor="#7FB448" style="padding-left: 10px;"></td>' +
    '<td class="white_text_14_bold" style="padding-left:10px; background-color:#7FB448;">' +
      'HIT Database Mk. II&nbsp;<a href="https://greasyfork.org/en/scripts/11733-mturk-hit-database-mk-ii" class="whatis" target="_blank">' +
      '(What\'s this?)</a></td></tr>' +
    '<tr><td class="container-content" colspan="2">' +
    '<div style="text-align:center;" id="hdbDashboardInterface">' +
    '<button id="hdbBackup" title="Export your entire database!\nPerfect for moving between computers or as a periodic backup">Create Backup</button>' +
    '<button id="hdbRestore" title="Restore database from external backup file" style="margin:5px">Restore</button>' +
    '<button id="hdbUpdate" title="Update... the database" style="color:green;">Update Database</button>' +
    '<div id="hdbFileSelector" style="display:none"><input id="hdbFileInput" type="file" /></div>' +
    '<br>' +
    '<button id="hdbPending" title="Summary of all pending HITs\n Can be exported as CSV" style="margin: 0px 5px 5px;">Pending Overview</button>' +
    '<button id="hdbRequester" title="Summary of all requesters\n Can be exported as CSV" style="margin: 0px 5px 5px;">Requester Overview</button>' +
    '<button id="hdbDaily" title="Summary of each day you\'ve worked\nCan be exported as CSV" style="margin:0px 5px 5px;">Daily Overview</button>' +
    '<br>' +
    '<label>Find </label>' +
    '<select id="hdbStatusSelect"><option value="*">ALL</option><option value="Approval" style="color: orange;">Pending Approval</option>' +
    '<option value="Rejected" style="color: red;">Rejected</option><option value="Approved" style="color:green;">Approved - Pending Payment</option>' +
    '<option value="(Paid|Approved)" style="color:green;">Paid OR Approved</option></select>' +
    '<label> HITs matching: </label><input id="hdbSearchInput" title="Query can be HIT title, HIT ID, or requester name" />' +
    '<button id="hdbSearch">Search</button>' +
    '<br>' +
    '<label>from date </label><input id="hdbMinDate" maxlength="10" size="10" title="Specify a date, or leave blank">' +
    '<label> to </label><input id="hdbMaxDate" malength="10" size="10" title="Specify a date, or leave blank">' +
    '<label for="hdbCSVInput" title="Export results as CSV file" style="margin-left:50px; vertical-align:middle;">export CSV</label>' +
    '<input id="hdbCSVInput" title="Export results as CSV file" type="checkbox" style="vertical-align:middle;">' +
    '<br>' +
    '<label id="hdbStatusText">placeholder status text</label>' +
    '<div id="hdbProgressBar" class="hdbProgressContainer"><div class="hdbProgressOuter"><div class="hdbProgressInner"></div></div></div>' +
    '</div></td></tr>';

  var updateBtn      = document.querySelector("#hdbUpdate"),
      backupBtn      = document.querySelector("#hdbBackup"),
      restoreBtn     = document.querySelector("#hdbRestore"),
      fileInput      = document.querySelector("#hdbFileInput"),
      exportCSVInput = document.querySelector("#hdbCSVInput"),
      searchBtn      = document.querySelector("#hdbSearch"),
      searchInput    = document.querySelector("#hdbSearchInput"),
      pendingBtn     = document.querySelector("#hdbPending"),
      reqBtn         = document.querySelector("#hdbRequester"),
      dailyBtn       = document.querySelector("#hdbDaily"),
      fromdate       = document.querySelector("#hdbMinDate"),
      todate         = document.querySelector("#hdbMaxDate"),
      statusSelect   = document.querySelector("#hdbStatusSelect"),
      progressBar    = document.querySelector("#hdbProgressBar");

  var searchResults  = document.createElement("DIV");
  searchResults.align = "center";
  searchResults.id = "hdbSearchResults";
  searchResults.style.display = "block";
  searchResults.innerHTML = 
    '<span style="border-bottom:1px solid;color:blue;cursor:pointer;display:none;">[ clear results ]</span><br>' +
    '<table cellSpacing="0" cellpadding="2" id="hdbResultsTable"></table>';
  document.body.insertBefore(searchResults, insertionNode);

  searchResults.firstChild.onclick = function(e) { 
    e.target.style.display = "none";
    searchResults.children[2].innerHTML = null;
  };

  updateBtn.onclick = function() { 
    progressBar.style.display = "block";
    metrics.dbupdate = new Metrics("database_update");
    HITStorage.fetch(MTURK_BASE+"status");
    document.querySelector("#hdbStatusText").textContent = "fetching status page....";
  };
  exportCSVInput.addEventListener("click", function() {
    if (exportCSVInput.checked) {
      searchBtn.textContent = "Export CSV";
      pendingBtn.textContent += " (csv)";
      reqBtn.textContent += " (csv)";
      dailyBtn.textContent += " (csv)";
    }
    else {
      searchBtn.textContent = "Search";
      pendingBtn.textContent = pendingBtn.textContent.replace(" (csv)","");
      reqBtn.textContent = reqBtn.textContent.replace(" (csv)","");
      dailyBtn.textContent = dailyBtn.textContent.replace(" (csv)", "");
    }
  });
  fromdate.addEventListener("focus", function() { 
    var offsets = getPosition(this, true);
    new Calendar(offsets.x, offsets.y, this).drawCalendar();
  });
  todate.addEventListener("focus", function() {
    var offsets = getPosition(this, true);
    new Calendar(offsets.x, offsets.y, this).drawCalendar();
  });

  backupBtn.onclick = HITStorage.backup;
  restoreBtn.onclick = function() { fileInput.click(); };
  fileInput.onchange = processFile;

  searchBtn.onclick = function() {
    var r = getRange();
    var _filter = { status: statusSelect.value, query: searchInput.value.trim().length > 0 ? searchInput.value : "*" };
    var _opt = { index: "date", range: r.range, dir: r.dir, filter: _filter, progress: true };

    metrics.dbrecall = new Metrics("database_recall::search");
    HITStorage.recall("HIT", _opt).then(function(r) {
      searchResults.children[0].style.display = "initial";
      searchResults.children[2].innerHTML = exportCSVInput.checked ? r.formatCSV() : r.formatHTML();
      autoScroll("#hdbSearchResults");

      for (var _r of r.results) { // retrieve and append notes
        HITStorage.recall("NOTES", { index: "hitId", range: window.IDBKeyRange.only(_r.hitId) }).then(noteHandler.bind(null,"attach"));
      }

      var el = null;
      for (el of document.querySelectorAll(".bonusCell")) {
        el.dataset.initial = el.textContent;
        el.onblur = updateBonus;
        el.onkeydown = updateBonus;
      }
      for (el of document.querySelectorAll('span[id^="note-"]')) {
        el.onclick = noteHandler.bind(null,"new");
      }
      metrics.dbrecall.stop(); metrics.dbrecall.report();
      progressBar.style.display = "none";
    });
  }; // search button click event
  pendingBtn.onclick = function() {
    var r = getRange();
    var _filter = { status: "Approval", query: searchInput.value.trim().length > 0 ? searchInput.value : "*" },
        _opt    = { index: "date", dir: "prev", range: r.range, filter: _filter, progress: true };

    metrics.dbrecall = new Metrics("database_recall::pending");
    HITStorage.recall("HIT", _opt).then(function(r) {
      searchResults.children[0].style.display = "initial";
      searchResults.children[2].innerHTML = exportCSVInput.checked ? r.formatCSV("pending") : r.formatHTML("pending");
      autoScroll("#hdbSearchResults");
      var expands = document.querySelectorAll(".hdbExpandRow");
      for (var el of expands) {
        el.onclick = showHiddenRows;
      }
      metrics.dbrecall.stop(); metrics.dbrecall.report();
      progressBar.style.display = "none";
    });
  }; //pending overview click event
  reqBtn.onclick = function() {
    var r = getRange();
    var _opt = { index: "date", range: r.range, progress: true };

    metrics.dbrecall = new Metrics("database_recall::requester");
    HITStorage.recall("HIT", _opt).then(function(r) {
      searchResults.children[0].style.display = "initial";
      searchResults.children[2].innerHTML = exportCSVInput.checked ? r.formatCSV("requester") : r.formatHTML("requester");
      autoScroll("#hdbSearchResults");
      var expands = document.querySelectorAll(".hdbExpandRow");
      for (var el of expands) {
        el.onclick = showHiddenRows;
      }
      metrics.dbrecall.stop(); metrics.dbrecall.report();
      progressBar.style.display = "none";
    });
  }; //requester overview click event
  dailyBtn.onclick = function() {
    metrics.dbrecall = new Metrics("database_recall::daily");
    HITStorage.recall("STATS", { dir: "prev" }).then(function(r) {
      searchResults.children[0].style.display = "initial";
      searchResults.children[2].innerHTML = exportCSVInput.checked ? r.formatCSV("daily") : r.formatHTML("daily");
      autoScroll("#hdbSearchResults");
      metrics.dbrecall.stop(); metrics.dbrecall.report();
    });
  }; //daily overview click event

  function getRange() {
    var _min = fromdate.value.length === 10 ? fromdate.value : undefined,
        _max = todate.value.length   === 10 ? todate.value   : undefined;
    var _range = 
      (_min === undefined && _max === undefined) ? null :
      (_min === undefined)                       ? window.IDBKeyRange.upperBound(_max) :
      (_max === undefined)                       ? window.IDBKeyRange.lowerBound(_min) :
      (_max < _min)                              ? window.IDBKeyRange.bound(_max,_min) : window.IDBKeyRange.bound(_min,_max);
    return { min: _min, max: _max, range: _range, dir: _max < _min ? "prev" : "next" };
  }
  function getPosition(element, includeHeight) {
    var offsets = { x: 0, y: includeHeight ? element.offsetHeight : 0 };
    do {
      offsets.x += element.offsetLeft;
      offsets.y += element.offsetTop;
      element = element.offsetParent;
    } while (element);
    return offsets;
  }
}//}}} dashboard

function showHiddenRows(e) {//{{{
  'use strict';

  var rid = e.target.parentNode.textContent.substr(4);
  var nodes = document.querySelectorAll('tr[data-rid="'+rid+'"]'), el = null;
  if (e.target.textContent === "[+]") {
    for (el of nodes)
      el.style.display="table-row";
    e.target.textContent = "[-]";
  } else {
    for (el of nodes)
      el.style.display="none";
    e.target.textContent = "[+]";
  }
}//}}}

function updateBonus(e) {//{{{
  'use strict';

  if (e instanceof window.KeyboardEvent && e.keyCode === 13) {
    e.target.blur();
    return false;
  } else if (e instanceof window.FocusEvent) {
    var _bonus = +e.target.textContent.replace(/\$/,"");
    if (_bonus !== +e.target.dataset.initial) {
      console.log("updating bonus to",_bonus,"from",e.target.dataset.initial,"("+e.target.dataset.hitid+")");
      e.target.dataset.initial = _bonus;
      var _pay   = +e.target.previousSibling.textContent,
          _range = window.IDBKeyRange.only(e.target.dataset.hitid);

      window.indexedDB.open(DB_NAME).onsuccess = function() {
        this.result.transaction("HIT", "readwrite").objectStore("HIT").openCursor(_range).onsuccess = function() {
          var c = this.result;
          if (c) {
            var v = c.value;
            v.reward = { pay: _pay, bonus: _bonus };
            c.update(v);
          } 
        }; // idbcursor
      }; // idbopen
    } // bonus is new value
  } // keycode
} //}}} updateBonus

function noteHandler(type, e) {//{{{
  // 
  // TODO restructure event handling/logic tree
  //      combine save and delete; it's ugly :(
  //      actually this whole thing is messy and in need of refactoring
  //
  'use strict';

  if (e instanceof window.KeyboardEvent) {
   if (e.keyCode === 13) {
      e.target.blur();
      return false;
    }
   return;
  }

  if (e instanceof window.FocusEvent) {
    if (e.target.textContent.trim() !== e.target.dataset.initial) {
      if (!e.target.textContent.trim()) { e.target.previousSibling.previousSibling.firstChild.click(); return; }
      var note   = e.target.textContent.trim(),
          _range = window.IDBKeyRange.only(e.target.dataset.id),
          inote  = e.target.dataset.initial,
          hitId  = e.target.dataset.id,
          date   = e.target.previousSibling.textContent;

      e.target.dataset.initial = note;
      window.indexedDB.open(DB_NAME).onsuccess = function() {
        this.result.transaction("NOTES", "readwrite").objectStore("NOTES").index("hitId").openCursor(_range).onsuccess = function() {
          if (this.result) {
            var r = this.result.value;
            if (r.note === inote) {  // note already exists in database, so we update its value
              r.note = note;
              this.result.update(r);
              return;
            }
            this.result.continue();
          } else {
            if (this.source instanceof window.IDBObjectStore)
              this.source.put({ note:note, date:date, hitId:hitId });
            else
              this.source.objectStore.put({ note:note, date:date, hitId:hitId });
          }
        };
        this.result.close();
      };
    } 
    return; // end of save event; no need to proceed
  }

  if (type === "delete") {
    var tr       = e.target.parentNode.parentNode,
        noteCell = tr.lastChild;
        _range   = window.IDBKeyRange.only(noteCell.dataset.id);
    if (!noteCell.dataset.initial) tr.remove();
    else {
      window.indexedDB.open(DB_NAME).onsuccess = function() {
        this.result.transaction("NOTES", "readwrite").objectStore("NOTES").index("hitId").openCursor(_range).onsuccess = function() {
          if (this.result) {
            if (this.result.value.note === noteCell.dataset.initial) {
              this.result.delete();
              tr.remove();
              return;
            }
            this.result.continue();
          } 
        };
        this.result.close();
      };
    }
    return; // end of deletion event; no need to proceed
  } else {
    if (type === "attach" && !e.results.length) return;

    var trow  = e instanceof window.MouseEvent ? e.target.parentNode.parentNode : null,
        tbody = trow ? trow.parentNode : null,
        row   = document.createElement("TR"),
        c1    = row.insertCell(0),
        c2    = row.insertCell(1),
        c3    = row.insertCell(2);
        date  = new Date();
        hitId = e instanceof window.MouseEvent ? e.target.id.substr(5) : null;

    c1.innerHTML = '<span class="removeNote" title="Delete this note" style="cursor:pointer;color:crimson;">[x]</span>';
    c1.firstChild.onclick = noteHandler.bind(null,"delete");
    c1.style.textAlign = "right";
    c2.title = "Date on which the note was added";
    c3.style.color = "crimson";
    c3.colSpan = "5";
    c3.contentEditable = "true";
    c3.onblur = noteHandler.bind(null,"blur");
    c3.onkeydown = noteHandler.bind(null, "kb");
    
    if (type === "new") {
      row.classList.add(trow.classList);
      tbody.insertBefore(row, trow.nextSibling);
      c2.textContent = date.getFullYear()+"-"+Number(date.getMonth()+1).toPadded()+"-"+Number(date.getDate()).toPadded();
      c3.dataset.initial = "";
      c3.dataset.id = hitId;
      c3.focus();
      return;
    }

    for (var entry of e.results) {
      trow  = document.querySelector('tr[data-id="'+entry.hitId+'"]');
      tbody = trow.parentNode;
      row   = row.cloneNode(true);
      c1    = row.firstChild;
      c2    = c1.nextSibling;
      c3    = row.lastChild;
      
      row.classList.add(trow.classList);
      tbody.insertBefore(row, trow.nextSibling);

      c1.firstChild.onclick = noteHandler.bind(null,"delete");
      c2.textContent = entry.date;
      c3.textContent = entry.note;
      c3.dataset.initial = entry.note;
      c3.dataset.id = entry.hitId;
      c3.onblur = noteHandler.bind(null,"blur");
      c3.onkeydown = noteHandler.bind(null, "kb");
    }
  } // new/attach
}//}}} noteHandler

function processFile(e) {//{{{
  'use strict';

  var f = e.target.files;
  if (f.length && f[0].name.search(/\.(bak|csv)$/) && ~f[0].type.search(/(text|json)/)) {
    var reader = new FileReader(), testing = true, isCsv = false;
    reader.readAsText(f[0].slice(0,10));
    reader.onload = function(e) { 
      if (testing && e.target.result.search(/(STATS|NOTES|HIT)/) < 0) {
        return error();
      } else if (testing) {
        testing = false;
        document.querySelector("#hdbProgressBar").style.display = "block";
        reader.readAsText(f[0]);
      } else {
        var data = JSON.parse(e.target.result);
        console.log(data);
        HITStorage.write(data, "restore");
      }
    }; // reader.onload
  } else {
    error();
  }

  function error() {
    var s = document.querySelector("#hdbStatusText"),
        e = "Restore::FileReadError : encountered unsupported file";
    s.style.color = "red";
    s.textContent = e;
    throw e;
  }
}//}}} processFile

function autoScroll(location, dt) {//{{{
  'use strict';

  var target = document.querySelector(location).offsetTop,
      pos    = window.scrollY,
      dpos   = Math.ceil((target - pos)/3);
  dt = dt ? dt-1 : 25; // time step/max recursions

  if (target === pos || dpos === 0 || dt === 0) return;

  window.scrollBy(0, dpos);
  setTimeout(function() { autoScroll(location, dt); }, dt);
}//}}}

function Calendar(offsetX, offsetY, caller) {//{{{
  'use strict';

  this.date = new Date();
  this.offsetX = offsetX;
  this.offsetY = offsetY;
  this.caller = caller;
  this.drawCalendar = function(year,month,day) {//{{{
    year = year || this.date.getFullYear();
    month = month || this.date.getMonth()+1;
    day = day || this.date.getDate();
    var longMonths = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
    var date = new Date(year,month-1,day);
    var anchors = _getAnchors(date);

    //make new container if one doesn't already exist
    var container = null;
    if (document.querySelector("#hdbCalendarPanel")) { 
      container = document.querySelector("#hdbCalendarPanel");
      container.removeChild( container.getElementsByTagName("TABLE")[0] );
    }
    else {
      container = document.createElement("DIV");
      container.id = "hdbCalendarPanel";
      document.body.appendChild(container);
    }
    container.style.left = this.offsetX;
    container.style.top = this.offsetY;
    var cal = document.createElement("TABLE");
    cal.cellSpacing = "0";
    cal.cellPadding = "0";
    cal.border = "0";
    container.appendChild(cal);
    cal.innerHTML = '<tr>' +
      '<th class="hdbCalHeader hdbCalControls" title="Previous month" style="text-align:right;"><span>&lt;</span></th>' +
      '<th class="hdbCalHeader hdbCalControls" title="Previous year" style="text-align:center;"><span>&#8810;</span></th>' +
      '<th colspan="3" id="hdbCalTableTitle" class="hdbCalHeader">'+date.getFullYear()+'<br>'+longMonths[date.getMonth()]+'</th>' +
      '<th class="hdbCalHeader hdbCalControls" title="Next year" style="text-align:center;"><span>&#8811;</span></th>' +
      '<th class="hdbCalHeader hdbCalControls" title="Next month" style="text-align:left;"><span>&gt;</span></th>' +
      '</tr><tr><th class="hdbDayHeader" style="color:red;">S</th><th class="hdbDayHeader">M</th>' +
      '<th class="hdbDayHeader">T</th><th class="hdbDayHeader">W</th><th class="hdbDayHeader">T</th>' +
      '<th class="hdbDayHeader">F</th><th class="hdbDayHeader">S</th></tr>';
    
    document.querySelector('th[title="Previous month"]').addEventListener( "click", function() { 
      this.drawCalendar(date.getFullYear(), date.getMonth(), 1);
    }.bind(this) );
    document.querySelector('th[title="Previous year"]').addEventListener( "click", function() {
      this.drawCalendar(date.getFullYear()-1, date.getMonth()+1, 1);
    }.bind(this) );
    document.querySelector('th[title="Next month"]').addEventListener( "click", function() {
      this.drawCalendar(date.getFullYear(), date.getMonth()+2, 1);
    }.bind(this) );
    document.querySelector('th[title="Next year"]').addEventListener( "click", function() {
      this.drawCalendar(date.getFullYear()+1, date.getMonth()+1, 1);
    }.bind(this) );

    var hasDay = false, thisDay = 1;
    for (var i=0;i<6;i++) { // cycle weeks
      var row = document.createElement("TR");
      for (var j=0;j<7;j++) { // cycle days
        if (!hasDay && j === anchors.first && thisDay < anchors.total)
          hasDay = true;
        else if (hasDay && thisDay > anchors.total)
          hasDay = false;

        var cell = document.createElement("TD");
        cell.classList.add("hdbCalCells");
        row.appendChild(cell);
        if (hasDay) {
          cell.classList.add("hdbCalDays");
          cell.textContent = thisDay;
          cell.addEventListener("click", _clickHandler.bind(this));
          cell.dataset.year = date.getFullYear();
          cell.dataset.month = date.getMonth()+1;
          cell.dataset.day = thisDay++;
        }
      } // for j
      cal.appendChild(row);
    } // for i

    function _clickHandler(e) {
      /*jshint validthis:true*/

      var y = e.target.dataset.year;
      var m = Number(e.target.dataset.month).toPadded();
      var d = Number(e.target.dataset.day).toPadded();
      this.caller.value = y+"-"+m+"-"+d;
      this.die();
    }

    function _getAnchors(date) {
      var _anchors = {};
      date.setMonth(date.getMonth()+1);
      date.setDate(0);
      _anchors.total = date.getDate();
      date.setDate(1);
      _anchors.first = date.getDay();
      return _anchors;
    }
  };//}}} drawCalendar

  this.die = function() { document.querySelector("#hdbCalendarPanel").remove(); };

}//}}} Calendar

// instance metrics apart from window scoped PerformanceTiming API
function Metrics(name) {//{{{
  'use strict';

  this.name = name || "undefined";
  this.marks = {};
  this.start = window.performance.now();
  this.end = null;
  this.stop = function(){
    if (!this.end) 
      this.end = window.performance.now();
    else
      throw "Metrics::AccessViolation: end point cannot be overwritten";
  };
  this.mark = function(name,position) {
    if (position === "end" && (!this.marks[name] || this.marks[name].end)) return;

    if (!this.marks[name])
      this.marks[name] = {};

    this.marks[name][position] = window.performance.now();
  };
  this.report = function() {
    console.group("Metrics for",this.name.toUpperCase());
    console.log("Process completed in",+Number((this.end-this.start)/1000).toFixed(3),"seconds");
    for (var k in this.marks) {
      if (this.marks.hasOwnProperty(k)) {
        console.log(k,"occurred after",+Number((this.marks[k].start-this.start)/1000).toFixed(3),"seconds,",
            "resolving in", +Number((this.marks[k].end-this.marks[k].start)/1000).toFixed(3), "seconds");
      }
    }
    console.groupEnd();
  };
}//}}}

/*
 *
 *
 * * * * * * * * * * * * * TESTING FUNCTIONS -- DELETE BEFORE FINAL RELEASE * * * * * * * * * * * 
 *
 *
 */

function INFLATEDUMMYVALUES() { //{{{
  'use strict';

  var tdb = this.result;
  tdb.onerror = function(e) { console.log("requesterror",e.target.error.name,e.target.error.message,e); };
  tdb.onversionchange = function(e) { console.log("tdb received versionchange request", e); tdb.close(); };
  //console.log(tdb.transaction("HIT").objectStore("HIT").indexNames.contains("date"));
  console.groupCollapsed("Populating test database");
  var tdbt = {};
  tdbt.trans = tdb.transaction(["HIT", "NOTES", "BLOCKS"], "readwrite");
  tdbt.hit   = tdbt.trans.objectStore("HIT");
  tdbt.notes = tdbt.trans.objectStore("NOTES");
  tdbt.blocks= tdbt.trans.objectStore("BLOCKS");

  var filler = { notes:[], hit:[], blocks:[]};
  for (var n=0;n<100000;n++) {
    filler.hit.push({ date: "2015-08-00", requesterName: "tReq"+(n+1), title: "Greatest Title Ever #"+(n+1), 
      reward: Number((n+1)%(200/n)+(((n+1)%200)/100)).toFixed(2), status: "moo",
      requesterId: ("RRRRRRR"+n).substr(-7), hitId: ("HHHHHHH"+n).substr(-7) });
    if (n%1000 === 0) {
      filler.notes.push({ requesterId: ("RRRRRRR"+n).substr(-7), note: n+1 +
        " Proin vel erat commodo mi interdum rhoncus. Sed lobortis porttitor arcu, et tristique ipsum semper a." +
        " Donec eget aliquet lectus, vel scelerisque ligula." });
      filler.blocks.push({requesterId: ("RRRRRRR"+n).substr(-7)});
    }
  }

  _write(tdbt.hit, filler.hit);
  _write(tdbt.notes, filler.notes);
  _write(tdbt.blocks, filler.blocks);

  function _write(store, obj) {
    if (obj.length) {
      var t = obj.pop();
      store.put(t).onsuccess = function() { _write(store, obj) };
    } else {
      console.log("population complete");
    }
  }

  console.groupEnd();

  dbh = window.indexedDB.open(DB_NAME, DB_VERSION);
  dbh.onerror = function(e) { console.log("[HITDB]",e.target.error.name+":", e.target.error.message, e); };
  console.log(dbh.readyState, dbh);
  dbh.onupgradeneeded = HITStorage.versionChange;
  dbh.onblocked = function(e) { console.log("blocked event triggered:", e); };

  tdb.close();

}//}}}

function BLANKSLATE() { //{{{ create empty db equivalent to original schema to test upgrade
    'use strict';
    var tdb = this.result;
    if (!tdb.objectStoreNames.contains("HIT")) { 
        console.log("creating HIT OS");
        var dbo = tdb.createObjectStore("HIT", { keyPath: "hitId" });
        dbo.createIndex("date", "date", { unique: false });
        dbo.createIndex("requesterName", "requesterName", { unique: false});
        dbo.createIndex("title", "title", { unique: false });
        dbo.createIndex("reward", "reward", { unique: false });
        dbo.createIndex("status", "status", { unique: false });
        dbo.createIndex("requesterId", "requesterId", { unique: false });

    }
    if (!tdb.objectStoreNames.contains("STATS")) {
        console.log("creating STATS OS");
        dbo = tdb.createObjectStore("STATS", { keyPath: "date" });
    }
    if (!tdb.objectStoreNames.contains("NOTES")) {
        console.log("creating NOTES OS");
        dbo = tdb.createObjectStore("NOTES", { keyPath: "requesterId" });
    }
    if (!tdb.objectStoreNames.contains("BLOCKS")) {
        console.log("creating BLOCKS OS");
        dbo = tdb.createObjectStore("BLOCKS", { keyPath: "id", autoIncrement: true });
        dbo.createIndex("requesterId", "requesterId", { unique: false });
    }
} //}}}



// vim: ts=2:sw=2:et:fdm=marker:noai