Greasy Fork is available in English.

MTurk HIT Database Mk.II

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

Verze ze dne 16. 08. 2015. Zobrazit nejnovější verzi.

// ==UserScript==
// @name         MTurk HIT Database Mk.II
// @author       feihtality
// @namespace    https://greasyfork.org/en/users/12709
// @version      0.7.417
// @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
 *   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: !!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(); };
    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 (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 = +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");
      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].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, "");
        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)
        };
      }

      // 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,
        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("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 && limit++ < 2000) { // limit to 2000 to save memory usage in large databases
            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))) )
              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("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();
      prog.style.display = "none";
    }

  }//}}} 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:#c60000;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) ? '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.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);}" +
"</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="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 = '<table cellSpacing="0" cellpadding="2"></table>';
  document.body.insertBefore(searchResults, insertionNode);

  updateBtn.onclick = function() { 
    progressBar.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 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 };

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

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

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