[MTurk Worker] Dashboard Enhancer

Brings many enhancements to the MTurk Worker Dashboard.

  1. // ==UserScript==
  2. // @name [MTurk Worker] Dashboard Enhancer
  3. // @namespace http://kadauchi.com/
  4. // @version 2.2.0
  5. // @description Brings many enhancements to the MTurk Worker Dashboard.
  6. // @author Kadauchi
  7. // @icon http://i.imgur.com/oGRQwPN.png
  8. // @include https://worker.mturk.com/dashboard*
  9. // ==/UserScript==
  10.  
  11. try { if (mturksuite) return; } catch (e) {}
  12.  
  13. const toNum = (string) => Number(string.replace(/[^0-9.]/g, ``));
  14. const toDate = (string) => string.split(`T`)[0];
  15. const toMoney = (number) => `$${number.toFixed(2)}`;
  16. const needPlus = (number) => number > 0 ? `+` : ``;
  17.  
  18. const statusDetailsTable = document.querySelector(
  19. `div[data-react-class="require('reactComponents/dailyWorkerStatisticsTable/DailyWorkerStatisticsTable')['default']"]`,
  20. );
  21. const statusDetailsArr = JSON.parse(statusDetailsTable.dataset.reactProps).bodyData;
  22. const statusDetailsObj = statusDetailsArr.reduce((obj, details) => ({ ...obj, [toDate(details.date)]: details }), {});
  23.  
  24. const hitsOverviewTable = document.getElementById(`dashboard-hits-overview`);
  25. const hitsOverviewRows = hitsOverviewTable.querySelectorAll(`.col-xs-5.text-xs-right`);
  26. const hitsOverview = {
  27. approved: toNum(hitsOverviewRows[0].textContent),
  28. pending: toNum(hitsOverviewRows[1].textContent),
  29. rejected: toNum(hitsOverviewRows[2].textContent),
  30. };
  31.  
  32. function allApprovedRate() {
  33. const row = document.createElement(`div`);
  34. row.className = `row m-b-sm`;
  35.  
  36. const col1 = document.createElement(`div`);
  37. col1.className = `col-xs-7`;
  38. row.appendChild(col1);
  39.  
  40. const strong = document.createElement(`strong`);
  41. strong.textContent = `All Approved Rate`;
  42. col1.appendChild(strong);
  43.  
  44. const col2 = document.createElement(`div`);
  45. col2.className = `col-xs-5 text-xs-right`;
  46. col2.textContent = `${(
  47. ((hitsOverview.approved + hitsOverview.pending) /
  48. (hitsOverview.approved + hitsOverview.pending + hitsOverview.rejected)) *
  49. 100
  50. ).toFixed(4)}%`;
  51. row.appendChild(col2);
  52.  
  53. const hr = document.getElementById(`dashboard-hits-overview`).getElementsByTagName(`hr`)[1];
  54. hr.parentNode.insertBefore(row, hr);
  55. }
  56.  
  57. function allRejectedRate() {
  58. const row = document.createElement(`div`);
  59. row.className = `row m-b-sm`;
  60.  
  61. const col1 = document.createElement(`div`);
  62. col1.className = `col-xs-7`;
  63. row.appendChild(col1);
  64.  
  65. const strong = document.createElement(`strong`);
  66. strong.textContent = `All Rejected Rate`;
  67. col1.appendChild(strong);
  68.  
  69. const col2 = document.createElement(`div`);
  70. col2.textContent = `${(
  71. (hitsOverview.approved / (hitsOverview.approved + hitsOverview.rejected + hitsOverview.pending)) *
  72. 100
  73. ).toFixed(4)}%`;
  74. col2.className = `col-xs-5 text-xs-right`;
  75. row.appendChild(col2);
  76.  
  77. const hr = document.getElementById(`dashboard-hits-overview`).getElementsByTagName(`hr`)[1];
  78. hr.parentNode.insertBefore(row, hr);
  79. }
  80.  
  81. function fourDigitPercents() {
  82. for (const row of document.getElementById(`dashboard-hits-overview`).getElementsByClassName(`row`)) {
  83. if (row.textContent.includes(`Approval Rate`)) {
  84. row.getElementsByClassName(`text-xs-right`)[0].textContent = `${(
  85. (hitsOverview.approved / (hitsOverview.approved + hitsOverview.rejected)) *
  86. 100
  87. ).toFixed(4)}%`;
  88. }
  89. if (row.textContent.includes(`Rejection Rate`)) {
  90. row.getElementsByClassName(`text-xs-right`)[0].textContent = `${(
  91. (hitsOverview.rejected / (hitsOverview.approved + hitsOverview.rejected)) *
  92. 100
  93. ).toFixed(4)}%`;
  94. }
  95. }
  96. }
  97.  
  98. function hitStatusChanges() {
  99. const old = localStorage.getItem(`statusDetailsObj`) ? JSON.parse(localStorage.getItem(`statusDetailsObj`)) : {};
  100. localStorage.setItem(`statusDetailsObj`, JSON.stringify(statusDetailsObj));
  101.  
  102. function applyChanges(node) {
  103. node.querySelectorAll(`.desktop-row`).forEach((row) => {
  104. const date = row.querySelector(`a`).href.split(`/status_details/`)[1];
  105.  
  106. row.querySelectorAll(`.text-xs-right`).forEach((col) => {
  107. const key = col.classList[2].replace(`-column`, ``).replace(`-`, `_`);
  108. const change = statusDetailsObj[date][key] - (old[date] ? old[date][key] : 0);
  109.  
  110. if (change !== 0) {
  111. const span = document.createElement(`span`);
  112. span.textContent = key.includes(`rewards`) || key.includes(`earnings`) ? `${needPlus(change)}${toMoney(change)}` : `${needPlus(change)}${change}`;
  113. span.style.float = `left`;
  114. span.style.fontSize = `70%`;
  115. col.appendChild(span);
  116. }
  117. });
  118. });
  119. }
  120.  
  121. const observer = new MutationObserver((mutationsList, observer) => {
  122. mutationsList.forEach((mutation) => {
  123. const addedNode = mutation.addedNodes[0];
  124.  
  125. if (addedNode && addedNode.classList.contains(`expanded-row`)) {
  126. applyChanges(addedNode);
  127. }
  128. });
  129. });
  130.  
  131. observer.observe(statusDetailsTable, { childList: true, subtree: true });
  132. }
  133.  
  134. function latestActivity() {
  135. const latest = statusDetailsArr[0];
  136. const date = toDate(latest.date);
  137.  
  138. const container = document.createElement(`div`);
  139. container.className = `row m-b-xl`;
  140.  
  141. const col = document.createElement(`div`);
  142. col.className = `col-xs-12`;
  143. container.appendChild(col);
  144.  
  145. const h2 = document.createElement(`h2`);
  146. h2.className = `m-b-md`;
  147. h2.textContent = `Activity for ${date}`;
  148. col.appendChild(h2);
  149.  
  150. const row = document.createElement(`div`);
  151. row.className = `row`;
  152. col.appendChild(row);
  153.  
  154. const col2 = document.createElement(`div`);
  155. col2.className = `col-xs-12`;
  156. row.appendChild(col2);
  157.  
  158. const border = document.createElement(`div`);
  159. border.className = `border-gray-lightest p-a-sm`;
  160. col2.appendChild(border);
  161.  
  162. const earningsRow = document.createElement(`div`);
  163. earningsRow.className = `row m-b-sm`;
  164. border.appendChild(earningsRow);
  165.  
  166. const earningsText = document.createElement(`div`);
  167. earningsText.className = `col-xs-7 col-sm-6 col-lg-7`;
  168. earningsRow.appendChild(earningsText);
  169.  
  170. const earningsStrong = document.createElement(`strong`);
  171. earningsStrong.textContent = `Projected Earnings`;
  172. earningsText.appendChild(earningsStrong);
  173.  
  174. const earningsValue = document.createElement(`div`);
  175. earningsValue.className = `col-xs-5 col-sm-6 col-lg-5 text-xs-right`;
  176. earningsValue.textContent = localStorage.todaysearnings || `$0.00`;
  177. earningsRow.appendChild(earningsValue);
  178.  
  179. const bonusesRow = document.createElement(`div`);
  180. bonusesRow.className = `row m-b-sm`;
  181. border.appendChild(bonusesRow);
  182.  
  183. const bonusesText = document.createElement(`div`);
  184. bonusesText.className = `col-xs-7 col-sm-6 col-lg-7`;
  185. bonusesRow.appendChild(bonusesText);
  186.  
  187. const bonusesStrong = document.createElement(`strong`);
  188. bonusesStrong.textContent = `Bonuses`;
  189. bonusesText.appendChild(bonusesStrong);
  190.  
  191. const bonusesValue = document.createElement(`div`);
  192. bonusesValue.className = `col-xs-5 col-sm-6 col-lg-5 text-xs-right`;
  193. bonusesValue.textContent = localStorage.todaysbonuses || `$0.00`;
  194. bonusesRow.appendChild(bonusesValue);
  195.  
  196. const collapse = document.createElement(`div`);
  197. collapse.id = `TodaysActivityAdditionalInfo`;
  198. collapse.className = `collapse`;
  199. border.appendChild(collapse);
  200.  
  201. const hr = document.createElement(`hr`);
  202. hr.className = `m-b-sm m-t-0`;
  203. collapse.appendChild(hr);
  204.  
  205. const hr2 = document.createElement(`hr`);
  206. hr2.className = `m-b-sm m-t-0`;
  207. border.appendChild(hr2);
  208.  
  209. const control = document.createElement(`a`);
  210. control.className = `collapse-more-less`;
  211. control.href = `#TodaysActivityAdditionalInfo`;
  212. control.setAttribute(`aria-controls`, `TodaysActivityAdditionalInfo`);
  213. control.setAttribute(`aria-expanded`, `false`);
  214. control.setAttribute(`data-toggle`, `collapse`);
  215. border.appendChild(control);
  216.  
  217. const more = document.createElement(`span`);
  218. more.className = `more`;
  219. control.appendChild(more);
  220.  
  221. const plus = document.createElement(`i`);
  222. plus.className = `fa fa-plus-circle`;
  223. more.appendChild(plus);
  224.  
  225. const moreText = document.createTextNode(`\nMore\n`);
  226. more.appendChild(moreText);
  227.  
  228. const less = document.createElement(`span`);
  229. less.className = `less`;
  230. control.appendChild(less);
  231.  
  232. const minus = document.createElement(`i`);
  233. minus.className = `fa fa-minus-circle`;
  234. less.appendChild(minus);
  235.  
  236. const lessText = document.createTextNode(`\nLess\n`);
  237. less.appendChild(lessText);
  238.  
  239. const side = document.querySelector(`.col-md-push-8`);
  240. side.insertBefore(container, side.firstChild);
  241.  
  242. bonusesValue.textContent = `$${latest.bonus_rewards.toLocaleString(`en-US`, { minimumFractionDigits: 2 })}`;
  243.  
  244. let hitLog =
  245. date === localStorage.WMTD_date ? (localStorage.WMTD_hitLog ? JSON.parse(localStorage.WMTD_hitLog) : {}) : {};
  246.  
  247. async function get(page, rescan) {
  248. try {
  249. page = Number.isInteger(page) ? page : 1;
  250.  
  251. earningsValue.textContent = `Calculating Page ${page}`;
  252.  
  253. const fetchURL = new URL(`https://worker.mturk.com/status_details/${date}`);
  254. fetchURL.searchParams.append(`page_number`, page);
  255. fetchURL.searchParams.append(`format`, `json`);
  256.  
  257. const response = await fetch(fetchURL, {
  258. credentials: `include`,
  259. });
  260.  
  261. if (response.status === 429) {
  262. return setTimeout(get, 2000, page, rescan);
  263. }
  264.  
  265. const json = await response.json();
  266.  
  267. for (const hit of json.results) {
  268. hitLog[hit.hit_id] = hit;
  269. }
  270.  
  271. const logLength = Object.keys(hitLog).length;
  272. const expectedLength = Number(page) * 20 - 20 + json.num_results;
  273.  
  274. if (!rescan && logLength !== expectedLength) {
  275. return get(1, true);
  276. } else {
  277. localStorage.WMTD_hitLog = JSON.stringify(hitLog);
  278. }
  279.  
  280. localStorage.WMTD_lastPage = page;
  281.  
  282. if (json.results.length === 20) {
  283. return get(++page, rescan);
  284. } else if (logLength !== json.total_num_results) {
  285. hitLog = {};
  286. return get(1, true);
  287. } else {
  288. let projectedEarnings = 0;
  289. const reqLog = {};
  290.  
  291. for (const key in hitLog) {
  292. const hit = hitLog[key];
  293.  
  294. if (hit.status !== `Rejected`) {
  295. projectedEarnings += hit.reward.amount_in_dollars;
  296. }
  297. if (!reqLog[hit.requester_id]) {
  298. reqLog[hit.requester_id] = {
  299. requester_id: hit.requester_id,
  300. requester_name: hit.requester_name,
  301. reward: hit.reward.amount_in_dollars,
  302. submitted: 1,
  303. };
  304. } else {
  305. reqLog[hit.requester_id].submitted += 1;
  306. reqLog[hit.requester_id].reward += hit.reward.amount_in_dollars;
  307. }
  308. }
  309.  
  310. const sort = Object.keys(reqLog).sort((a, b) => reqLog[a].reward - reqLog[b].reward);
  311.  
  312. const fragment = document.createDocumentFragment();
  313.  
  314. for (let i = sort.length - 1; i > -1; i--) {
  315. const key = sort[i];
  316. const requester_name = reqLog[key].requester_name;
  317. const reward = `$${reqLog[key].reward.toLocaleString(`en-US`, { minimumFractionDigits: 2 })}`;
  318. const submitted = reqLog[key].submitted;
  319.  
  320. const reqRow = document.createElement(`div`);
  321. reqRow.className = `row m-b-sm`;
  322. fragment.appendChild(reqRow);
  323.  
  324. const requester = document.createElement(`div`);
  325. requester.className = `col-xs-6`;
  326. reqRow.appendChild(requester);
  327.  
  328. const requesterStrong = document.createElement(`strong`);
  329. requesterStrong.textContent = requester_name;
  330. requester.appendChild(requesterStrong);
  331.  
  332. const submitValue = document.createElement(`div`);
  333. submitValue.className = `col-xs-3 text-xs-right`;
  334. submitValue.textContent = submitted;
  335. reqRow.appendChild(submitValue);
  336.  
  337. const rewardValue = document.createElement(`div`);
  338. rewardValue.className = `col-xs-3 text-xs-right`;
  339. rewardValue.textContent = reward;
  340. reqRow.appendChild(rewardValue);
  341. }
  342.  
  343. collapse.appendChild(fragment);
  344.  
  345. earningsValue.textContent = `$${projectedEarnings.toLocaleString(`en-US`, { minimumFractionDigits: 2 })}`;
  346. }
  347. } catch (error) {
  348. earningsValue.textContent = error;
  349. }
  350. }
  351.  
  352. get(
  353. date === localStorage.WMTD_date ? (localStorage.WMTD_lastPage ? Number(localStorage.WMTD_lastPage) : 1) : 1,
  354. false,
  355. );
  356. localStorage.WMTD_date = date;
  357. }
  358.  
  359. function openFirstWeek() {
  360. statusDetailsTable.querySelector(`.fa.expand-button.fa-plus-circle`).click();
  361. }
  362.  
  363. function rejectionsBelow99() {
  364. const row = document.createElement(`div`);
  365. row.className = `row m-b-sm`;
  366.  
  367. const col1 = document.createElement(`div`);
  368. col1.className = `col-xs-7`;
  369. row.appendChild(col1);
  370.  
  371. const strong = document.createElement(`strong`);
  372. strong.textContent = `Rejections 99%`;
  373. col1.appendChild(strong);
  374.  
  375. const col2 = document.createElement(`div`);
  376. col2.textContent = Math.round(
  377. (hitsOverview.rejected - 0.01 * (hitsOverview.approved + hitsOverview.rejected + hitsOverview.pending)) / -0.99,
  378. ).toLocaleString();
  379. col2.className = `col-xs-5 text-xs-right`;
  380. row.appendChild(col2);
  381.  
  382. const additional = document
  383. .getElementById(`dashboard-hits-overview`)
  384. .getElementsByClassName(`border-gray-lightest`)[0];
  385. additional.appendChild(row);
  386. }
  387.  
  388. function rejectionsBelow95() {
  389. const row = document.createElement(`div`);
  390. row.className = `row m-b-sm`;
  391.  
  392. const col1 = document.createElement(`div`);
  393. col1.className = `col-xs-7`;
  394. row.appendChild(col1);
  395.  
  396. const strong = document.createElement(`strong`);
  397. strong.textContent = `Rejections 95%`;
  398. col1.appendChild(strong);
  399.  
  400. const col2 = document.createElement(`div`);
  401. col2.textContent = Math.round(
  402. (hitsOverview.rejected - 0.05 * (hitsOverview.approved + hitsOverview.rejected + hitsOverview.pending)) / -0.95,
  403. ).toLocaleString();
  404. col2.className = `col-xs-5 text-xs-right`;
  405. row.appendChild(col2);
  406.  
  407. const additional = document
  408. .getElementById(`dashboard-hits-overview`)
  409. .getElementsByClassName(`border-gray-lightest`)[0];
  410. additional.appendChild(row);
  411. }
  412.  
  413. allApprovedRate();
  414. allRejectedRate();
  415. fourDigitPercents();
  416. hitStatusChanges();
  417. latestActivity();
  418. openFirstWeek();
  419. rejectionsBelow99();
  420. rejectionsBelow95();