|
| 1 | +<!DOCTYPE html> |
| 2 | +<html> |
| 3 | +<head> |
| 4 | + <meta charset="UTF-8"> |
| 5 | + <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src-elem 'self' 'unsafe-inline'; script-src 'self' https://cdnjs.cloudflare.com 'unsafe-inline' 'unsafe-eval'; img-src 'self' data:"> |
| 6 | + <title>Pseudo-Anonymised ID Generator</title> |
| 7 | + <link rel="icon" type="image/x-icon" href="favicon.png"> |
| 8 | + <style> |
| 9 | + * { box-sizing: border-box; } |
| 10 | + body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #fff; min-height: 100vh; color: #000; line-height: 1.5; padding: 40px 20px; margin: 0; } |
| 11 | + .container { max-width: 800px; margin: 0 auto; } |
| 12 | + #privacyNotice { color: #c00; font-weight: 600; text-align: center; margin-bottom: 30px; font-size: 14px; } |
| 13 | + h1 { font-size: 24px; font-weight: 600; margin-bottom: 30px; text-align: center; } |
| 14 | + h2 { font-size: 16px; font-weight: 600; margin-bottom: 15px; border-bottom: 1px solid #000; padding-bottom: 8px; } |
| 15 | + .card { margin-bottom: 30px; } |
| 16 | + .upload-area { border: 1px dashed #999; padding: 30px; text-align: center; cursor: pointer; } |
| 17 | + .upload-area:hover { border-color: #000; } |
| 18 | + .upload-text { font-size: 14px; color: #666; } |
| 19 | + .file-name { font-weight: 500; margin-top: 10px; } |
| 20 | + .btn { background: #000; color: #fff; border: none; padding: 10px 24px; font-size: 14px; cursor: pointer; } |
| 21 | + .btn:hover { background: #333; } |
| 22 | + .btn:disabled { background: #ccc; cursor: not-allowed; } |
| 23 | + .btn-process { width: 100%; margin-top: 20px; padding: 12px; } |
| 24 | + .column-options { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 8px; } |
| 25 | + .column-option { display: flex; align-items: center; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee; } |
| 26 | + .column-option label { font-size: 14px; } |
| 27 | + select { background: #fff; border: 1px solid #000; padding: 6px 10px; font-size: 14px; cursor: pointer; } |
| 28 | + select:focus { outline: none; } |
| 29 | + table { width: 100%; border-collapse: collapse; margin-top: 15px; font-size: 13px; } |
| 30 | + th, td { padding: 8px 12px; text-align: left; border: 1px solid #ddd; } |
| 31 | + th { background: #f5f5f5; font-weight: 600; } |
| 32 | + .preview-label { color: #666; font-size: 13px; } |
| 33 | + .table-container { overflow-x: auto; } |
| 34 | + #hashInputContainer { display: flex; flex-direction: column; gap: 10px; } |
| 35 | + .hash-input-row { display: flex; gap: 10px; } |
| 36 | + input[type="text"] { flex: 1; border: 1px solid #000; padding: 10px; font-size: 14px; } |
| 37 | + input[type="text"]:focus { outline: none; } |
| 38 | + #hashOutput { font-family: monospace; background: #f5f5f5; padding: 10px; font-size: 13px; min-height: 38px; } |
| 39 | + .loading-indicator { text-align: center; padding: 20px; color: #666; display: none; } |
| 40 | + footer { text-align: center; padding: 30px 0; color: #666; font-size: 13px; border-top: 1px solid #eee; margin-top: 30px; } |
| 41 | + footer a { color: #000; text-decoration: none; } |
| 42 | + footer a:hover { text-decoration: underline; } |
| 43 | + input[type="file"] { display: none; } |
| 44 | + #options, #preview { display: none; } |
| 45 | + </style> |
| 46 | +</head> |
| 47 | +<body> |
| 48 | + <div class="container"> |
| 49 | + <div id="privacyNotice">All processing is done locally. No data leaves your device.</div> |
| 50 | + <h1>Pseudo-Anonymised ID Generator</h1> |
| 51 | + <div class="card"> |
| 52 | + <form id="myForm"> |
| 53 | + <div class="upload-area" id="uploadArea"> |
| 54 | + <div class="upload-text">Drop CSV file here or click to browse</div> |
| 55 | + <input type="file" id="csvFile" accept=".csv"> |
| 56 | + <div class="file-name" id="fileName"></div> |
| 57 | + </div> |
| 58 | + <div id="loading" class="loading-indicator">Loading...</div> |
| 59 | + <div id="hashing" class="loading-indicator">Processing...</div> |
| 60 | + <button type="submit" class="btn btn-process" id="processBtn" disabled>Process and Download</button> |
| 61 | + </form> |
| 62 | + </div> |
| 63 | + <div id="options" class="card"> |
| 64 | + <h2>Column Options</h2> |
| 65 | + <div id="columnBoxes" class="column-options"></div> |
| 66 | + </div> |
| 67 | + <div id="preview" class="card"> |
| 68 | + <h2>Data Preview</h2> |
| 69 | + <p id="previewLbl" class="preview-label">Showing 0 of 0 rows</p> |
| 70 | + <div class="table-container"> |
| 71 | + <table id="previewTable"><thead><tr id="previewHeadRow"></tr></thead><tbody id="previewTableBody"></tbody></table> |
| 72 | + </div> |
| 73 | + </div> |
| 74 | + <div class="card" id="hashInputContainer"> |
| 75 | + <h2>Quick Hash Tool</h2> |
| 76 | + <div class="hash-input-row"> |
| 77 | + <input type="text" id="hashInput" placeholder="Enter text to hash..."> |
| 78 | + <button class="btn" id="hashBtn">Hash</button> |
| 79 | + </div> |
| 80 | + <div id="hashOutput"></div> |
| 81 | + </div> |
| 82 | + <footer> |
| 83 | + <a href="https://github.com/cai4cai/sha1inbrowser/blob/main/README.md">Instructions</a> · |
| 84 | + <a href="https://cai4cai.ml/terms/">Terms</a> · |
| 85 | + <a href="https://cai4cai.ml/privacy/">Privacy</a> · |
| 86 | + <a href="https://github.com/cai4cai/sha1inbrowser">GitHub</a> |
| 87 | + </footer> |
| 88 | + </div> |
| 89 | + |
| 90 | + <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js" integrity="sha512-vc58qvvBdrDR4etbxMdlTt4GBQk1qjvyORR2nrsPsFPyrs+/u5c3+1Ct6upOgdZoIl7eq6k3a1UPDSNAQi/32A==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> |
| 91 | + <script> |
| 92 | + async function sha1(str) { |
| 93 | + const enc = new TextEncoder(); |
| 94 | + const hash = await crypto.subtle.digest('SHA-1', enc.encode(str)); |
| 95 | + return Array.from(new Uint8Array(hash)).map(v => v.toString(16).padStart(2, '0')).join('').slice(0, 10); |
| 96 | + } |
| 97 | + |
| 98 | + const csvFile = document.getElementById("csvFile"); |
| 99 | + const table = document.getElementById("previewTable"); |
| 100 | + const uploadArea = document.getElementById("uploadArea"); |
| 101 | + const processBtn = document.getElementById("processBtn"); |
| 102 | + const fileNameDisplay = document.getElementById("fileName"); |
| 103 | + const hashColour = "#e0e0e0", excludeColour = "#999", stripeWidth = 6; |
| 104 | + |
| 105 | + document.getElementById("hashBtn").onclick = async () => { |
| 106 | + document.getElementById("hashOutput").innerText = "Hash: " + await sha1(document.getElementById("hashInput").value); |
| 107 | + }; |
| 108 | + |
| 109 | + uploadArea.onclick = () => csvFile.click(); |
| 110 | + uploadArea.ondragover = e => { e.preventDefault(); uploadArea.style.borderColor = "#000"; }; |
| 111 | + uploadArea.ondragleave = () => uploadArea.style.borderColor = "#999"; |
| 112 | + uploadArea.ondrop = e => { |
| 113 | + e.preventDefault(); |
| 114 | + uploadArea.style.borderColor = "#999"; |
| 115 | + if (e.dataTransfer.files[0]?.name.endsWith('.csv')) { |
| 116 | + csvFile.files = e.dataTransfer.files; |
| 117 | + csvFile.dispatchEvent(new Event('change')); |
| 118 | + } |
| 119 | + }; |
| 120 | + |
| 121 | + document.getElementById("myForm").onsubmit = async e => { |
| 122 | + e.preventDefault(); |
| 123 | + document.getElementById("hashing").style.display = "block"; |
| 124 | + const text = await csvFile.files[0].text(); |
| 125 | + const data = d3.csvParse(text), dataWithHash = d3.csvParse(text); |
| 126 | + const toHash = [], toHashAndExclude = [], toExclude = []; |
| 127 | + |
| 128 | + data.columns.forEach(col => { |
| 129 | + const val = document.getElementById("select_" + col).value; |
| 130 | + if (val === "Hash") toHash.push(col); |
| 131 | + else if (val === "Hash and exclude") toHashAndExclude.push(col); |
| 132 | + else if (val === "Exclude") toExclude.push(col); |
| 133 | + }); |
| 134 | + |
| 135 | + for (let i = 0; i < data.length; i++) { |
| 136 | + for (const col of toHash) { const h = await sha1(data[i][col]); data[i][col + "_hash"] = h; dataWithHash[i][col + "_hash"] = h; } |
| 137 | + for (const col of toHashAndExclude) { const h = await sha1(data[i][col]); data[i][col + "_hash"] = h; dataWithHash[i][col + "_hash"] = h; } |
| 138 | + toExclude.concat(toHashAndExclude).forEach(col => delete data[i][col]); |
| 139 | + } |
| 140 | + |
| 141 | + await window.electronAPI.saveFile(d3.csvFormat(data), "unidentifiable.csv"); |
| 142 | + await window.electronAPI.saveFile(d3.csvFormat(dataWithHash), "original_with_hash.csv"); |
| 143 | + document.getElementById("hashing").style.display = "none"; |
| 144 | + }; |
| 145 | + |
| 146 | + csvFile.onchange = async () => { |
| 147 | + const input = csvFile.files[0]; |
| 148 | + table.tBodies[0].innerHTML = ""; |
| 149 | + document.getElementById("previewHeadRow").innerHTML = ""; |
| 150 | + document.getElementById("columnBoxes").innerHTML = ""; |
| 151 | + |
| 152 | + if (!input) { |
| 153 | + document.getElementById("options").style.display = "none"; |
| 154 | + document.getElementById("preview").style.display = "none"; |
| 155 | + processBtn.disabled = true; |
| 156 | + fileNameDisplay.textContent = ""; |
| 157 | + return; |
| 158 | + } |
| 159 | + |
| 160 | + fileNameDisplay.textContent = input.name; |
| 161 | + document.getElementById("loading").style.display = "block"; |
| 162 | + |
| 163 | + const data = d3.csvParse(await input.text()); |
| 164 | + const header = document.getElementById("previewHeadRow"); |
| 165 | + data.columns.forEach((col, i) => { const c = header.insertCell(i); c.id = "cell_" + col + "_0"; c.innerHTML = col; }); |
| 166 | + |
| 167 | + const toShow = Math.min(data.length, 10); |
| 168 | + for (let i = 0; i < toShow; i++) { |
| 169 | + const row = table.tBodies[0].insertRow(i); |
| 170 | + data.columns.forEach((col, j) => { |
| 171 | + let content = data[i][col]; |
| 172 | + if (content.length > 25) content = content.slice(0, 22).trim() + "..."; |
| 173 | + const c = row.insertCell(j); c.id = "cell_" + col + "_" + (i + 1); c.innerHTML = content; |
| 174 | + }); |
| 175 | + } |
| 176 | + |
| 177 | + const parent = document.getElementById("columnBoxes"); |
| 178 | + data.columns.forEach(name => { |
| 179 | + const wrapper = document.createElement("div"); wrapper.className = "column-option"; |
| 180 | + const label = document.createElement("label"); label.textContent = name; |
| 181 | + const select = document.createElement("select"); select.id = "select_" + name; |
| 182 | + ["Keep", "Exclude", "Hash", "Hash and exclude"].forEach(opt => { const o = document.createElement("option"); o.value = o.text = opt; select.appendChild(o); }); |
| 183 | + select.onchange = () => { |
| 184 | + const val = select.value.toLowerCase(), toHash = val.includes("hash"), toExclude = val.includes("exclude"); |
| 185 | + for (let i = 0; i < table.rows.length; i++) { |
| 186 | + const cell = document.getElementById("cell_" + name + "_" + i); |
| 187 | + cell.style.backgroundImage = `repeating-linear-gradient(58deg, ${toHash ? hashColour : "transparent"}, ${toHash ? hashColour : "transparent"} ${stripeWidth}px, ${toExclude ? excludeColour : "transparent"} ${stripeWidth}px, ${toExclude ? excludeColour : "transparent"} ${stripeWidth * 2}px)`; |
| 188 | + } |
| 189 | + }; |
| 190 | + wrapper.append(label, select); parent.appendChild(wrapper); |
| 191 | + }); |
| 192 | + |
| 193 | + document.getElementById("previewLbl").innerHTML = `Showing ${toShow} of ${data.length} rows`; |
| 194 | + document.getElementById("options").style.display = "block"; |
| 195 | + document.getElementById("preview").style.display = "block"; |
| 196 | + document.getElementById("loading").style.display = "none"; |
| 197 | + processBtn.disabled = false; |
| 198 | + }; |
| 199 | + </script> |
| 200 | +</body> |
| 201 | +</html> |
0 commit comments