MTurk HIT Database Mk.II

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

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         MTurk HIT Database Mk.II
// @author       feihtality
// @namespace    https://greasyfork.org/en/users/12709
// @version      0.7.337
// @description  Keep track of the HITs you've done (and more!)
// @include      /^https://www\.mturk\.com/mturk/(dash|view|sort|find|prev|search).*/
// @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
 *   projected earnings
 *   note functionality
 *   migrate blocks to notes
 *   tagging (?)
 *   searching via R/T buttons
 *
 */



const DB_VERSION = 2;
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);
};

var qc = { extraDays: Boolean(localStorage.getItem("hitdb_extraDays")) || false, seen: {} };
if (localStorage.getItem("hitdb_fetchData"))
  qc.fetchData = JSON.parse(localStorage.getItem("hitdb_fetchData"));
else
  qc.fetchData = {};

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(); console.log("closing hitdb"); };
    var dbo;

    console.groupCollapsed("HITStorage.versionChange::onupgradeneeded", this === dbh);

    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 });
    }

    /* probably not as useful as originally conceptualized
    if (!db.objectStoreNames.contains("REQUESTER")) { // new in v5: new object store
      console.log("creating REQUESTER OS");
      dbo = db.createObjectStore("REQUESTER", { keyPath: "requesterId" });
      dbo.createIndex("tobydate", "tobydate", { unique: false });
      dbo.createIndex("notes", "notes", { 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);

    (function _updateBlocks(dbt) { // new in v5: schema change
      if (db.objectStoreNames.contains("BLOCKS"))
        _mv(db, dbt, "BLOCKS", "BLOCKS", _updateBlocks);
      else {
        console.log("creating BLOCKS OS");
        dbo = db.createObjectStore("BLOCKS", { keyPath: "requesterId" });
        dbo.createIndex("requesterName", "requesterName", { unique: false });
      }
    })(this.transaction);

    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 (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";

    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 (doc.querySelector('td[class="error_title"]')) // no more status information
      parseMisc("end");
    else 
      throw "ParseError::unhandled document received @"+doc.documentURI;


    function parseStatus() {//{{{
      HITStorage.data = { HIT: [], STATS: [] };
      qc.seen = {};
      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      = HITStorage.ISODate(_date);
        d.submitted = Number(raw.sub[i].innerText);
        d.approved  = Number(raw.app[i].innerText);
        d.rejected  = Number(raw.rej[i].innerText);
        d.pending   = Number(raw.pen[i].innerText);
        d.earnings  = Number(raw.pay[i].innerText.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) >= 0) ) {
            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");
      console.log("page:", _page, "date:", _date);
      statusLabel.textContent = "Processing "+HITStorage.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          = HITStorage.ISODate(_date);
        d.feedback      = raw.feedback[i].innerText.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].innerText.trim().replace(/\|/g,"");
        d.reward        = Number(raw.pay[i].innerText.substr(1));
        d.status        = raw.status[i].innerText;
        d.title         = raw.title[i].innerText.replace(/\|/g, "");
        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) >= 0) ? 
            (qc.seen[_date].pending + 1 || 1) : (qc.seen[_date].pending || 0)
        };
      }

      // 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 (HITStorage.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));
        }
        // 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.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(HITStorage.ISODate(_date)));
      }
    }//}}} parseDetail

    function parseMisc(type) {//{{{
      var d = doc.documentURI.replace(/.+(\d{8}).+/, "$1");
      var payload = { encodedDate: _decDate(HITStorage.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 URL -- how did you end up here??";
    }//}}}

    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
  
  ISODate: function(date) { //{{{ MMDDYYYY -> YYYY-MM-DD
    'use strict';

    return date.substr(4)+"-"+date.substr(0,2)+"-"+date.substr(2,2);
  }, //}}} ISODate

  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';

    var dbh = window.indexedDB.open("HITDB_TESTING");
    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";
      }
    }

  }, //}}} 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 !== "*" ? options.filter.status : false : false) : false,
        fq    = options ? (options.filter ? options.filter.query  !== "*" ? new RegExp(options.filter.query,"i")  : false : false) : false;

    var sr = new DatabaseResult();
    return new Promise( function(resolve) {
      window.indexedDB.open("HITDB_TESTING").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) >= 0) ||  // status match and no query filter OR
                 (!fs && fq &&                                     // query match and no status filter OR
                   (c.value.title.search(fq) >= 0 || c.value.requesterName.search(fq) >= 0 || c.value.hitId.search(fq) >= 0))  ||
                 (fs && fq && c.value.status.search(fs) >= 0 &&    // status match and query match
                   (c.value.title.search(fq) >= 0 || c.value.requesterName.search(fq) >= 0 || c.value.hitId.search(fq) >= 0)) )
              sr.include(c.value);
            c.continue();
          } else
            resolve(sr);
        };
      };
    } ); // promise
  },//}}} recall

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

    var bData = {},
        os    = ["STATS", "NOTES", "HIT"],
        count = 0;

    window.indexedDB.open("HITDB_TESTING").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:""});
      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";
      dl.click();
    }

  }//}}} backup

};//}}} HITStorage

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

  this.results = [];
  this.formatHTML = function(type) {
    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('<tr style="background:#7fb448;font-size:12px;color:white"><th>Date</th><th>Submitted</th>' +
          '<th>Approved</th><th>Rejected</th><th>Pending</th><th>Earnings</th></tr>');
      for (entry of this.results) {
        _trClass = (count++ % 2 === 0) ? 'class="even"' : 'class="odd"';
        
        htmlTxt.push('<tr '+_trClass+' align="center"><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>');
      }
    } else if (type === "pending" || type === "requester") {
      htmlTxt.push('<tr data-sort="99999" style="background:#7fb448;font-size:12px;color:white"><th>Requester ID</th>' +
          '<th width="504px">Requester</th><th>' + (type === "pending" ? 'Pending' : 'HITs') + '</th><th>Rewards</th></tr>');
      var r = {};
      for (entry of this.results) {
        if (!r[entry.requesterId]) r[entry.requesterId] = [];
        r[entry.requesterId].push(entry);
        r[entry.requesterId].pay = r[entry.requesterId].pay ? 
          typeof entry.reward === "object" ? r[entry.requesterId].pay + (+entry.reward.pay) : r[entry.requesterId].pay + (+entry.reward) :
          typeof entry.reward === "object" ? +entry.reward.pay : +entry.reward;
      }
      for (var k in r) {
        if (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>' + r[k].length + '</td><td>' + Number(Math.decRound(r[k].pay,2)).toFixed(2) + '</td></tr>'];
          for (var hit of r[k]) {
            tr.push('<tr data-rid="'+r[k][0].requesterId+'" style="color:orange;display:none;"><td align="right">' + hit.date + '</td>' +
                '<td max-width="504px">' + hit.title + '</td><td></td><td align="right">' +
                (typeof hit.reward === "object" ? Number(hit.reward.pay).toFixed(2) : Number(hit.reward).toFixed(2)) +
                '</td></tr>');
          }
          htmlTxt.push(tr.join(''));
        }
      }
      htmlTxt.sort(function(a,b) { return +b.substr(15,5).match(/\d+/) - +a.substr(15,5).match(/\d+/); });
    } else { // default
      htmlTxt.push('<tr style="background:#7FB448;font-size:12px;color:white"><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="2"></th></tr>'+
          '<tr style="background:#7FB448;font-size:12px;color:white">' +
          '<th>Date</th><th>Requester</th><th>HIT title</th><th style="font-size:10px;">Pay</th>'+
          '<th style="font-size:10px;">Bonus</th><th>Status</th><th>Feedback</th></tr>');

      for (entry of this.results) {
        _trClass = (count++ % 2 === 0) ? 'class="even"' : 'class="odd"';
        var _stColor = entry.status.search(/(paid|approved)/i) >= 0 ? 'style="color:green;"'  :
                       entry.status === "Pending Approval"          ? 'style="color:orange;"' : 'style="color:red;"';

        htmlTxt.push("<tr "+_trClass+"><td width=\"74px\">" + entry.date + "</td><td style=\"max-width:145px;\">" + entry.requesterName + 
            "</td><td width='375px' title='HIT ID:   "+entry.hitId+"'>" + entry.title + "</td><td>" +
            (typeof entry.reward === "object" ? Number(entry.reward.pay).toFixed(2) : Number(entry.reward).toFixed(2)) + 
            "</td><td width='36px' contenteditable='true' data-hitid='"+entry.hitId+"'>" + 
            (typeof entry.reward === "object" ? Number(entry.reward.bonus).toFixed(2) : "&nbsp;") + 
            "</td><td "+_stColor+">" + entry.status + "</td><td>" + entry.feedback + "</td></tr>");
      }
    }
    return htmlTxt.join('');
  }; // formatHTML
  this.formatCSV = function(type) {};
  this.include = function(value) {
    this.results.push(value);
  };
}//}}} 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("HITDB_TESTING");
  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;
  */

  var dbh = window.indexedDB.open("HITDB_TESTING", DB_VERSION);
  dbh.onerror = function(e) { 'use strict'; console.log("[HITDB]",e.target.error.name+":", e.target.error.message, e); };
  dbh.onupgradeneeded = HITStorage.versionChange;

  if (document.location.pathname.search(/dashboard/) > 0)
    dashboardUI();
  else
    beenThereDoneThat();

  //FILEREADERANDBACKUPTESTING();

}
/*}}}
 *
 *    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.6s 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 2.7s 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);}" +
"</style>";
document.head.innerHTML += css;
// }}}

function beenThereDoneThat() {//{{{
  // 
  // TODO add search on button click
  //
  'use strict';

  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");
    cellR.innerHTML = '<span class="capsule_field_title">Auto-Approval:</span>&nbsp;&nbsp;'+_ftime(autoApproval);
    var rbutton = document.createElement("BUTTON");
    rbutton.classList.add("hitdbRTButtons","hitdbRTButtons-large");
    rbutton.textContent = "Requester";
    rbutton.onclick = function(e) { e.preventDefault(); };
    var tbutton = rbutton.cloneNode(false);
    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");
      insertionNodes.push(requesterNodes[i].parentNode.parentNode.parentNode);

      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");
      _div.appendChild(_tbutton);
      _tbutton.textContent = 'T';
      _tbutton.classList.add("hitdbRTButtons");
      insertionNodes[i].appendChild(_tr);
    }
  } // else

  function processResults(r) {
    /*jshint validthis: true*/
    if (r.results.length) this.classList.add("hitdbRTButtons-green");
  }

  function _ftime(t) {
    var d = Math.floor(t/86400);
    var h = Math.floor(t%86400/3600);
    var m = Math.floor(t%86400%3600/60);
    var 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" : "");
  }

}//}}} 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="TODO PUTLINKTOSCRIPTHERE" 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");

  var searchResults  = document.createElement("DIV");
  searchResults.align = "center";
  searchResults.id = "hdbSearchResults";
  searchResults.style.display = "block";
  searchResults.innerHTML = '<table cellSpacing="0" cellpadding="2"></table>';
  document.body.insertBefore(searchResults, insertionNode);

  updateBtn.onclick = function() { 
    document.querySelector("#hdbProgressBar").style.display = "block";
    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 offsetX = this.offsetLeft + this.offsetParent.offsetLeft + this.offsetParent.offsetParent.offsetLeft;
    var offsetY = this.offsetHeight + this.offsetTop + this.offsetParent.offsetTop + this.offsetParent.offsetParent.offsetTop;
    new Calendar(offsetX, offsetY, this).drawCalendar();
  });
  todate.addEventListener("focus", function() {
    var offsetX = this.offsetLeft + this.offsetParent.offsetLeft + this.offsetParent.offsetParent.offsetLeft;
    var offsetY = this.offsetHeight + this.offsetTop + this.offsetParent.offsetTop + this.offsetParent.offsetParent.offsetTop;
    new Calendar(offsetX, offsetY, 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 };

    HITStorage.recall("HIT", _opt).then(function(r) {
      searchResults.firstChild.innerHTML = r.formatHTML();
      autoScroll("#hdbSearchResults", 0.5);
      var bonusCells = document.querySelectorAll('td[contenteditable="true"]');
      for (var el of bonusCells) {
        el.dataset.storedValue = el.textContent;
        el.onblur = updateBonus;
        el.onkeydown = updateBonus;
      }
    });
  }; // 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 };

    HITStorage.recall("HIT", _opt).then(function(r) {
      searchResults.firstChild.innerHTML = r.formatHTML("pending");
      autoScroll("#hdbSearchResults", 0.5);
      var expands = document.querySelectorAll(".hdbExpandRow");
      for (var el of expands) {
        el.onclick = showHiddenRows;
      }
    });
  }; //pending overview click event
  reqBtn.onclick = function() {
    var r = getRange();
    var _opt = { index: "date", range: r.range };

    HITStorage.recall("HIT", _opt).then(function(r) {
      searchResults.firstChild.innerHTML = r.formatHTML("requester");
      autoScroll("#hdbSearchResults", 0.5);
      var expands = document.querySelectorAll(".hdbExpandRow");
      for (var el of expands) {
        el.onclick = showHiddenRows;
      }
    });
  }; //requester overview click event
  dailyBtn.onclick = function() {
    HITStorage.recall("STATS", { dir: "prev" }).then(function(r) {
      searchResults.firstChild.innerHTML = r.formatHTML("daily");
      autoScroll("#hdbSearchResults", 0.5);
    });
  }; //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" };
  }
}//}}} dashboard

function showHiddenRows(e) {//{{{
  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) {//{{{
  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.storedValue) {
      console.log("updating bonus to",_bonus,"from",e.target.dataset.storedValue,"("+e.target.dataset.hitid+")");
      e.target.dataset.storedValue = _bonus;
      var _pay   = +e.target.previousSibling.textContent,
          _range = window.IDBKeyRange.only(e.target.dataset.hitid);

      window.indexedDB.open("HITDB_TESTING").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 processFile(e) {//{{{
  'use strict';

  var f = e.target.files;
  if (f.length && f[0].name.search(/\.bak$/) && f[0].type.search(/text/) >= 0) {
    var reader = new FileReader(), testing = true;
    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;
        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

// super simple super shitty autoscroll
// TODO make it better 
//
function autoScroll(location, time) {//{{{
  'use strict';

  var target = document.querySelector(location).offsetTop,
      pos    = window.scrollY,
      dpos   = Math.floor((target-pos)/300) || 1;
  
  var timer = setInterval(function() {
    if (window.scrollY >= target)
      clearInterval(timer);
    else 
      window.scrollBy(0, dpos);
  }, time*1000/300);
  setTimeout(function() { clearInterval(timer); }, 2000);
}//}}}

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
/*
 *
 *
 * * * * * * * * * * * * * TESTING FUNCTIONS -- DELETE BEFORE FINAL RELEASE * * * * * * * * * * * 
 *
 *
 */
function FILEREADERANDBACKUPTESTING() {//{{{
  'use strict';
  
  var testdiv = document.createElement("DIV");
  var resultsdiv = document.createElement("DIV");
  document.body.appendChild(testdiv);
  var gobtn = document.createElement("BUTTON");
  var fileinput = document.createElement("INPUT");
  var reader = new FileReader();
  var osinput = document.createElement("INPUT");
  var osgobtn = document.createElement("BUTTON");
  var count = count || 0;
  osgobtn.textContent = "get object store";
  
  osgobtn.onclick = function() {
    var os = osinput.value || null;
    var backupdata = {};
    if (os) {
      window.indexedDB.open("HITDB_TESTING").onsuccess = function() {
        if (os === "ALL") {
          os = ["BLOCKS", "STATS", "REQUESTER", "HIT", "NOTES"];
          for (var store of os) {
            this.result.transaction(os, "readonly").objectStore(store).openCursor(null).onsuccess = testbackup;
          }
        }
        else {
          var results = [];
          this.result.transaction(os, "readonly").objectStore(os).openCursor(null).onsuccess = function() {
            var cursor = this.result;
            if (cursor) {
              results.push(JSON.stringify(cursor.value));
              cursor.continue();
            } else {
              resultsdiv.innerHTML = results.join("<br>");
              console.log(results);
            }
          }; // cursor
        } // else not "ALL"
      }; //opendb
    } //if os specified
    function testbackup(event) {
      var cursor = event.target.result;
      if (cursor) {
        if (!backupdata[cursor.source.name]) backupdata[cursor.source.name] = [];
        backupdata[cursor.source.name].push(JSON.stringify(cursor.value));
        cursor.continue();
      } else
        if (++count === 5)
          //console.log(count, backupdata);
          finalizebackup();
    }
    function finalizebackup() {
      var backupblob = new Blob([JSON.stringify(backupdata)], {type:""});
      var dl = document.createElement("A");
      dl.href = URL.createObjectURL(backupblob);
      console.log(dl.href);
      dl.download = "hitdb.bak";
      dl.click();
    }
  }; // btn click event

  fileinput.type = "file";
  testdiv.appendChild(fileinput);
  testdiv.appendChild(document.createTextNode("test"));
  testdiv.appendChild(gobtn);
  testdiv.appendChild(osinput);
  testdiv.appendChild(osgobtn);
  testdiv.appendChild(resultsdiv);
  gobtn.textContent = "Go!";
  resultsdiv.style.display = "block";
  resultsdiv.style.height = "500px";
  resultsdiv.style.textAlign = "left";
  testdiv.align = "center";
  gobtn.onclick = function() {
    console.log(fileinput.files);
    if (fileinput.files.length)
      //reader.readAsText(fileinput.files[0].slice(0,100)); // read first 100 chars
      reader.readAsText(fileinput.files[0]);
  };
  reader.onload = function(e) { 
    console.log("e:",e);
    console.log(reader);
    var resultsarray = reader.result.split("\n").length;
    var resultsobj = JSON.parse(reader.result);
    console.log(resultsarray);
    console.dir(resultsobj);
    resultsdiv.innerText = reader.result;
  };

  //var reader = new FileReader();
}//}}}

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"], "readwrite");
  tdbt.hit   = tdbt.trans.objectStore("HIT");
  tdbt.notes = tdbt.trans.objectStore("NOTES");

  var filler = { notes:[], hit:[] };
  filler.hit = [ "VoabFy5lUU", "1YgeT67IA9", "vWWOyoFAqJ", "jLlCRxKz5p", "2SNUvi93dA", "A01lbJiwD8", "oMimeCWfxp", "QKw7FvgOwo", "uyGaJWIWWk", "pWX0scGCSt", "iPMSBc47Im", "xD50vGi673", "s8zWC32Kt1", "HFtsDs5pv5", "Q9LLk54nH7", "k48IZrzHRs", "YK0Dhz2j1C", "TJfulNCQu4", "j8PZOUXYyK", "7TYIcl0L4C", "UEMqzXEcKc", "qWl6bY6GNL", "Ri5kDFdvaN", "szNKzcOxZD", "Lrxfrft5qI", "1LLVpUtctA", "6293TYywcc", "W06f7ryxEM", "iZjA2xrBR3", "9FGASc8Pom", "mlsOnX48fa", "gY1LtPRL5o", "gjSoG7SuNc", "wN2Oe6shHl", "ipUmlyUl16", "I5VxbkDHB2", "wi65vT5uUN", "7Z2MBa4ENj", "22wxAdweow", "2X0dXdXHyd", "xC8TDB5cFQ", "1nBE25uOvA", "zoOqlS2ZEx", "lwyQxcLyq3", "twXlQKGfoE", "F2DfGwdRH4", "nVipEYetgJ", "KNYx7cygqM", "of7sDJ6H6d", "q5AYDYHNMH", "dM4q1Y5tsD", "qLHZ4gPzpk", "Ld0OFYVna3", "UUzWV4E9LC", "fFouuKYuHp", "fmMS8SfPmH", "FgpLUQ2p2E", "KxG8HXguEi", "9zEBEh9xBV", "JAgMcHx2Xc", "MRHyspFK3u", "MEDJ6uPdQB", "JH4EmjMbxL", "Qzl3j1KcVX", "cbFdjEYtdo", "AX6IFb90wl", "P1Ff6mP9xg", "nGrB37OA2V", "1I357HgGPt", "1MzkqbY8DS", "r0JQ8wJ3Ur", "VI8RPAnUIF", "2LnPsDuVX7", "irh6nwUabW", "00Kt7IIZYn", "Iy9Dvs1bnB", "5LlLmogUaq", "l1qiV0KbCO", "cQR4R58ZTr", "8V47azrgmS", "wzO3FS8LHM", "U3Ku7FPoyu", "napyb1f9VY", "ooZzDumPvp", "7skPeH8vla", "RGHM0x2j2M", "nJkq3skoyg", "2jnF4CikUJ", "utRKO2Oshr", "2IU9SODFih", "BYfKqUNWhV", "5NcTE7596z", "wK0x7Luu53", "LMDNJ04xJz", "0F74zkwi2w", "HphbzLPf1S", "OxQqFBrpp7", "bMdRhznSxH", "iGmg2oOJxN", "SesnnXeLPI", "79fe44Kb9c", "NpgwXyrrcK", "pOSSCx00fb", "kYK54kD2za", "H1bPCpg4A7", "J68EJkY9ne", "SxRpzZEZos", "D8cbgEjtey", "4anYvaDsDL", "lNEyOn3Vex", "tjuogfu05L", "hihCQKD4bQ", "d2ErwlETC9", "PW0uJzoAYX", "xpQGjomGWt", "mv5ltGwzKd", "gIHxK3AUGd", "YSMsXWfO9A", "vov6vHFEy7", "yPKCBNKGQI", "0sVvhZBc4o", "GmwzWrns3l", "08AxVt9Jgm", "EelfjFWL5j", "W1mExuAAqI", "1llR8p71Db", "uIDgZJReUD", "ewJEXOCPvW", "8xkJ1R8CYC", "lFUNkNW6d6", "8O4Jf7zaV7", "MRa7r4dKnN", "dHnAN0PVrW", "e2b4V7rf6H", "Hoyt7FmEOh", "THfHyQtVNa", "YHZQ6kJdEh", "fAY0sUnAbh", "pkIKEpNG1M", "1KPIYkWMFX", "rnUYAGhkFD", "H8GohMjCX0", "kkTPxNjid1", "NDoKue8sQg", "yTiDDQgSuz", "vivbmfMYOE", "mUpXfjVI73", "JHDPUd1KKH", "VonOWCil0v", "gyWT2eyWmA", "zo8GnpUZ6M", "A8nUb4mGIA", "ZAvPl6NRtK", "j4FUAyxa00", "qMs29meBHd", "ZKdfBrwxXP", "ZVmV4RFn16", "dx5cpjHPyR", "v6dVaGCh8y", "3mSfFodGz1", "6Ri9l0FaOB", "0vfB82zPVF", "l6jlEjZUyF", "GcMwBp7NzM", "AWhuz5kNFH", "gLT9xUymoi", "cnLFBaitPy", "AzhTLXPAqf", "ZAZX7cqys9", "msNr9hEDJv", "wPRhQO24Qb", "asw7U6Fi9S", "aTvtJb8wRB", "eJvKjU7TbT", "fdoIJBbs6T", "AnmIvMa9uF", "C0BLUDfIQc", "MnTewsCCFJ", "KPLBtZhoGs", "1dFo4F1HyM", "6Hw8eBctcg", "dUKPwrAbDU", "A9eZcIVQqn", "de9c0BS7vg", "jsld2IhPlk", "PeTIygZ29t", "PMarJ1nfxI", "dg4qBOuqWd", "GitkUmOTEI", "O943VysSKC", "wwvvZet8rZ", "ceUsOJac8R", "Z5KyuKzlTA", "zUmNf2FiNP", "bMLQMNWa9Y", "kEixoD58jO", "NlPiPLvMIp", "mnHx8F3my1", "FRv2lY3KCZ", "1TCSGa1GNj" ]; 
  for (var n=0;n<filler.hit.length;n++) {
    filler.hit[n] = { date: "2015-08-02", requesterName: "tReq"+(n+1), title: "Best HIT Title #"+(n+1), 
      reward: Number((n+1)%(200/n)+(((n+1)%200)/100)).toFixed(2), status: "moo",
      requesterId: String(Math.random(n+1)*312679).replace(".",""), hitId: filler.hit[n] };
    filler.notes[n] = { requesterId: filler.hit[n].requesterId, 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." };
  }

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

  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("HITDB_TESTING", 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