MTurk HIT Database Mk.II

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

目前为 2015-08-15 提交的版本。查看 最新版本

// ==UserScript==
// @name         MTurk HIT Database Mk.II
// @author       feihtality
// @namespace    https://gf.qytechs.cn/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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址