├── .gitignore ├── CNAME ├── .travis.yml ├── README.md ├── styles.css ├── package.json ├── index.html ├── src ├── index.js └── lib.js ├── verify.html └── test └── libTest.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | accountable-triage.cs.jhu.edu -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - node 5 | script: 6 | - npm test 7 | os: linux 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Accountable Triage 2 | 3 | ## Development Setup 4 | 5 | Make sure you have Node.js and NPM installed. 6 | 7 | Run `npm install` to install dependencies, `npm start` to start the server. 8 | 9 | Do NOT edit `index.js` directly; instead edit files in `src` and then run `npm 10 | run build`. 11 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 20px; 3 | padding: 20px; 4 | } 5 | 6 | li { 7 | margin: 10px; 8 | } 9 | 10 | input { 11 | width: 200px; 12 | font-size: 18px; 13 | } 14 | 15 | button { 16 | display: block; 17 | margin-top: 15px; 18 | margin-bottom: 15px; 19 | font-size: 13px; 20 | } 21 | 22 | #results { 23 | padding-top: 15px; 24 | padding-bottom: 15px; 25 | } 26 | 27 | #dates { 28 | margin-top: 25px; 29 | margin-bottom: 10px; 30 | } 31 | 32 | #date { 33 | font-size: 18px; 34 | } 35 | 36 | #submit { 37 | margin-top: 20px; 38 | } 39 | 40 | .indent { 41 | margin-left: 50px; 42 | } 43 | 44 | #resultsContainer { 45 | width: 600px; 46 | margin: 50px; 47 | display: none; 48 | } 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "accountable-triage", 3 | "version": "0.0.1", 4 | "description": "TBD", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "start": "npm run build && python3 -m http.server 8000 --bind 127.0.0.1", 8 | "test": "mochify --timeout 5000", 9 | "watch": "mochify --watch", 10 | "webdriver": "mochify --wd", 11 | "lint": "standard", 12 | "build": "browserify src/*.js -o index.js" 13 | }, 14 | "author": "", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "browserify": "^16.5.0", 18 | "mochify": "^6.6.0" 19 | }, 20 | "dependencies": { 21 | "jspdf": "^2.3.1", 22 | "sha3": "^2.1.2" 23 | }, 24 | "standard": { 25 | "ignore": [ 26 | "index.js", 27 | "dist/" 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Accountable Triage 8 | 9 | 10 |
Enter one patient MRN per row:
11 |
    12 |
  1. 13 |
  2. 14 |
  3. 15 |
16 |
17 | 18 | 19 |
20 |
21 |

Highest to lowest priority:

22 |
    23 |
24 |
25 |
26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const lib = require('./lib') 3 | const { jsPDF } = require('jspdf') 4 | 5 | window.onload = () => { 6 | const e = { 7 | ids: document.getElementById('ids'), 8 | submit: document.getElementById('submit'), 9 | addRow: document.getElementById('addRow'), 10 | date: document.getElementById('date'), 11 | resultsContainer: document.getElementById('resultsContainer'), 12 | results: document.getElementById('results') 13 | } 14 | 15 | const generatePDF = (day) => { 16 | var pdf = new jsPDF('p', 'pt', 'a4') 17 | pdf.html(e.resultsContainer, { 18 | html2canvas: { 19 | scale: 0.9 20 | }, 21 | callback: (doc) => { 22 | doc.save(`lottery-${day}.pdf`) 23 | } 24 | }) 25 | } 26 | 27 | e.submit.onclick = () => { 28 | const dateInput = document.getElementById('dateInput') 29 | const tzInput = document.getElementById('tzInput') 30 | const d = new Date() 31 | const day = dateInput ? dateInput.value : lib.getDate(d) 32 | const tzCode = tzInput ? tzInput.value : lib.getTZCode(d) 33 | if (!day) { 34 | window.alert('Please enter a valid date.') 35 | return 36 | } 37 | if (!tzCode) { 38 | window.alert('Please enter a valid time zone.') 39 | return 40 | } 41 | e.results.innerHTML = '' 42 | e.resultsContainer.style.display = 'none' 43 | const array = [] 44 | const inputs = ids.querySelectorAll('input') 45 | inputs.forEach((input) => { 46 | array.push(input.value) 47 | }) 48 | lib.permuteIDs(array, day, tzCode).then((result) => { 49 | result.forEach((item) => { 50 | const li = document.createElement('li') 51 | li.innerText = item 52 | e.results.appendChild(li) 53 | }) 54 | e.resultsContainer.style.display = 'block' 55 | if (e.date) { 56 | e.date.innerText = `Results generated on: ${d.toString()}` 57 | generatePDF(day) 58 | } 59 | }).catch((e) => { 60 | console.log(e) 61 | alert('Error getting results.') 62 | }) 63 | } 64 | 65 | e.addRow.onclick = () => { 66 | const li = document.createElement('li') 67 | const input = document.createElement('input') 68 | input.type = 'text' 69 | li.appendChild(input) 70 | e.ids.appendChild(li) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /verify.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Verify triage results 8 | 9 | 10 |
Enter one patient MRN per row:
11 |
    12 |
  1. 13 |
  2. 14 |
  3. 15 |
16 | 17 |
Enter lottery date:
18 | 19 |
Enter local time zone where the lottery was run:
20 | 49 | 50 |
51 |

Highest to lowest priority:

52 |
    53 |
54 |
55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/lib.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const { SHA3 } = require('sha3') 3 | 4 | /** 5 | * Converts UTC midnight of local YYYY-MM-DD string to ms since 1970-01-01 UTC 6 | * and gets NIST beacon for that time. 7 | * @param {string} dateString 8 | * @returns {number} 9 | */ 10 | const fetchNistBeacon = async (dateString, tzCode) => { 11 | if (!tzCode || tzCode.length !== 6 || tzCode[3] !== ':') { 12 | throw new Error('Invalid time zone.') 13 | } 14 | const date = Date.parse(`${dateString}T00:00:00.000${tzCode}`) 15 | if (!date) { 16 | throw new Error('Invalid date.') 17 | } 18 | const url = `https://beacon.nist.gov/beacon/2.0/pulse/time/${date}` 19 | const resp = await window.fetch(url) 20 | const json = await resp.json() 21 | return json.pulse.outputValue 22 | } 23 | 24 | module.exports = { 25 | /** 26 | * Gets SHA3 hash of some input string. 27 | * @param {string} input 28 | * @returns {string} 29 | */ 30 | hash: (input) => { 31 | const hash = new SHA3(512) 32 | hash.update(input) 33 | return hash.digest('hex') 34 | }, 35 | 36 | /** 37 | * Takes hash of date + normalized ID, copys it to a map of hash to ID 38 | * @param {string} id 39 | * @param {string} salt 40 | * @param {Object} outputs 41 | */ 42 | idToHash: (id, salt, outputs) => { 43 | if (typeof id !== 'string') { 44 | return 45 | } 46 | id = id.replace(/\s/g, '').toLowerCase() 47 | if (id.length) { 48 | const input = [salt, id].join('|') 49 | outputs[module.exports.hash(input)] = id 50 | } 51 | }, 52 | 53 | /** 54 | * Computes a permutation of the IDs based on the hash 55 | * @param {Array} ids 56 | * @param {string} date 57 | * @returns {Array} 58 | */ 59 | permuteIDs: async (ids, date, tzCode) => { 60 | const salt = await fetchNistBeacon(date, tzCode) 61 | const hashesToIds = {} 62 | ids.forEach((id) => module.exports.idToHash(id, salt, hashesToIds)) 63 | // Sorts lexicographically. This should be equivalent to 64 | // sorting numerically in this case since hashes are equal length hex. 65 | // Ex: ['0f', 'a'].sort() => ['0f', 'a'] 66 | // ['0f', '0a'].sort() => ['0a', '0f'] 67 | const sortedHashes = Object.keys(hashesToIds).sort() 68 | // Return array of IDs sorted by hash 69 | return sortedHashes.map(hash => hashesToIds[hash]) 70 | }, 71 | 72 | /** 73 | * Gets current day (YYYY-MM-DD) in local time 74 | * @returns {string} 75 | */ 76 | getDate: (d) => { 77 | d = d || new Date() 78 | const month = String(d.getMonth() + 1) 79 | const date = String(d.getDate()) 80 | const monthString = month.length < 2 ? `0${month}` : month 81 | const dateString = date.length < 2 ? `0${date}` : date 82 | return [d.getFullYear(), monthString, dateString].join('-') 83 | }, 84 | 85 | /** 86 | * Returns local timezone's offset from UTC as a string 87 | * +/- HH:MM, e.g. -04:00 88 | */ 89 | getTZCode: (d) => { 90 | d = d || new Date() 91 | const s = d.toString().split('GMT')[1] 92 | return [s.slice(0, 3), s.slice(3, 5)].join(':') 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /test/libTest.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const assert = require('assert') 5 | const lib = require('../src/lib') 6 | 7 | describe('hash', () => { 8 | it('returns known output', () => { 9 | assert.strict.equal( 10 | lib.hash('abc'), 11 | 'b751850b1a57168a5693cd924b6b096e08f621827444f70d884f5d0240d2712e10e116e9192af3c91a7ec57647e3934057340b4cf408d5a56592f8274eec53f0' 12 | ) 13 | assert.strict.equal( 14 | lib.hash(''), 15 | 'a69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26' 16 | ) 17 | assert.strict.equal( 18 | lib.hash('abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu'), 19 | 'afebb2ef542e6579c50cad06d2e578f9f8dd6881d7dc824d26360feebf18a4fa73e3261122948efcfd492e74e82e2189ed0fb440d187f382270cb455f21dd185' 20 | ) 21 | }) 22 | it('throws on non-string input', () => { 23 | assert.throws(() => { 24 | lib.hash(123) 25 | }) 26 | }) 27 | }) 28 | 29 | describe('idToHash', () => { 30 | it('normalizes the ID', () => { 31 | const date = '2020-04-04' 32 | const output = {} 33 | lib.idToHash('', date, output) 34 | lib.idToHash(' ', date, output) 35 | lib.idToHash(undefined, date, output) 36 | lib.idToHash('123', date, output) 37 | lib.idToHash(' 4 5 6 ', date, output) 38 | lib.idToHash('aZUki', date, output) 39 | lib.idToHash('ABC 11111 0000000000', date, output) 40 | assert.strict.deepEqual(output, { 41 | '0c395fe62e46cd4ac547437d8f10ad897b61781ee31530fcc2acded9ede3c1e7539a578e161289bf4b5693254eca6f4c0dc190cf1728a2aab3daf742de74d5c8': 'abc111110000000000', 42 | 'a39878927fe717f4f0135e37a2ea55b0d03b638921fc35aeec00897d4213c4f2c104389c2d820d4e5cd8b8364e772ed8a9f7e1831f1444799184a8412a2b5ccd': '123', 43 | '385317ff17c0acfd6c92f8fbb11f8af1390b97733183f19bc02be5c71eb7d23bfa1ebd7f3a16e980df6a8fd470dc9ae3296e097d9b5084cbd46d1d3908cccec4': '456', 44 | '822af1ded907b8b455210b2c99005eb9a87f34c21c9e70951da4d33f8b7434e03e8563c2bcc209109ad1194553809cb844bfdc18c858d355bc9bdae6feb15656': 'azuki' 45 | }) 46 | }) 47 | }) 48 | 49 | describe('permuteIDs', () => { 50 | const tzCode = '-07:00' 51 | it('is deterministic', async () => { 52 | const date = '2020-04-19' 53 | const res = await lib.permuteIDs(['hello', 'this', 'is', 'number', '42'], date, tzCode) 54 | const res2 = await lib.permuteIDs(['hello', 'this', 'is', 'number', '42'], date, tzCode) 55 | assert.strict.deepEqual(res, ['hello', '42', 'this', 'number', 'is']) 56 | assert.strict.deepEqual(res2, res) 57 | }) 58 | it('filters out dupes and invalid inputs', async () => { 59 | const date = '2019-12-31' 60 | const res = await lib.permuteIDs(['hello', '', 'this', 'is', 'number', 42, 'hello'], date, tzCode) 61 | assert.strict.deepEqual(res, ['number', 'hello', 'is', 'this']) 62 | }) 63 | it('gives same results given a fixed date', async () => { 64 | const date = '1983-12-01' 65 | const res1 = await lib.permuteIDs(['1', '2', '3'], date, tzCode) 66 | const res2 = await lib.permuteIDs(['1', '2', '3'], date, tzCode) 67 | assert.strict.deepEqual(res1, ['2', '1', '3']) 68 | assert.strict.deepEqual(res2, res1) 69 | const res3 = await lib.permuteIDs(['1', '2', '3'], '2020-01-01', tzCode) 70 | assert.strict.deepEqual(res3, ['1', '3', '2']) 71 | }) 72 | }) 73 | 74 | describe('getDate', () => { 75 | it('returns a correctly formatted string', () => { 76 | const date = lib.getDate() 77 | const split = date.split('-') 78 | assert(split.length === 3) 79 | assert(split[0].length === 4) 80 | assert(split[1].length === 2) 81 | assert(split[2].length === 2) 82 | }) 83 | it('gets correct date', () => { 84 | // note this is time zone dependent 85 | let date = lib.getDate(new Date(1587265965761)) 86 | assert.strict.equal(date, '2020-04-18') 87 | date = lib.getDate(new Date(0)) 88 | assert.strict.equal(date, '1969-12-31') 89 | }) 90 | }) 91 | 92 | describe('getTZCode', () => { 93 | it('no args', () => { 94 | const tz = lib.getTZCode() 95 | assert.strict.equal(tz.length, 6) 96 | assert.strict.equal(tz[3], ':') 97 | assert(['-', '+'].includes(tz[0])) 98 | }) 99 | it('args', () => { 100 | const tz = lib.getTZCode(new Date(1587265965761)) 101 | assert.strict.equal(tz.length, 6) 102 | assert.strict.equal(tz[3], ':') 103 | assert(['-', '+'].includes(tz[0])) 104 | const tz2 = lib.getTZCode(new Date(1587506737836)) 105 | assert.strict.equal(tz, tz2) 106 | const tz3 = lib.getTZCode(new Date('2010-05-05T07:03:51+0800')) 107 | assert.strict.equal(tz3, tz2) 108 | }) 109 | }) 110 | --------------------------------------------------------------------------------