From 95f552a0e54a248334c9622fc5b6bf4243e80d82 Mon Sep 17 00:00:00 2001 From: Mikhail Chusavitin Date: Thu, 3 Sep 2026 10:19:44 +0300 Subject: [PATCH] feat(privacy): fold sanitize into the header "Remove personal data" button Drops the in-panel sanitize sub-block (button + preview table + "already sanitized" note) that duplicated affordances across the page. The header "Clear Data" button is renamed "Remove personal data" and now sanitizes the uploaded file and downloads the clean copy in one click - no preview, no confirmation. It is shown only when the source can be sanitized, and reads "Personal data removed" (disabled) when the file already looks de-identified. The old clearData() teardown is covered by Restart. Co-Authored-By: Claude Sonnet 5 --- web/static/css/style.css | 57 ------------------ web/static/js/app.js | 124 ++++++++------------------------------- web/templates/index.html | 6 +- 3 files changed, 27 insertions(+), 160 deletions(-) diff --git a/web/static/css/style.css b/web/static/css/style.css index b87d5e1..792a945 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -1047,60 +1047,3 @@ code { color: var(--muted); } -.privacy-sanitize { - padding: 10px 12px; - border-bottom: 1px solid var(--border); -} - -.privacy-sanitize.hidden, -.privacy-sanitize-preview.hidden { - display: none; -} - -#privacy-sanitize-btn, -.privacy-sanitize-dl { - font-size: 0.85em; - padding: 5px 12px; - cursor: pointer; -} - -.privacy-sanitize-preview { - margin-top: 10px; - font-size: 0.88em; -} - -.privacy-sanitize-preview table { - margin: 6px 0; -} - -.privacy-sanitize-dl { - margin-top: 8px; - font-weight: 600; -} - -.privacy-sanitize-warn { - margin-top: 8px; - padding: 6px 8px; - background: #fff8f0; - border: 1px solid #f0e0c0; - border-radius: 4px; -} - -.privacy-sanitize-warn ul { - margin: 4px 0 0; - padding-left: 18px; - font-family: monospace; - font-size: 0.82em; -} - -.privacy-sanitize-err { - color: var(--crit-fg); -} - -.privacy-sanitized-note { - padding: 6px 8px; - background: #eef7ee; - border: 1px solid #cfe6cf; - border-radius: 4px; - font-size: 0.88em; -} diff --git a/web/static/js/app.js b/web/static/js/app.js index 854928e..7204411 100644 --- a/web/static/js/app.js +++ b/web/static/js/app.js @@ -1407,7 +1407,6 @@ let auditViewerNonce = 0; async function loadData(vendor, filename) { document.getElementById('upload-section').classList.add('hidden'); document.getElementById('data-section').classList.remove('hidden'); - document.getElementById('clear-btn').classList.remove('hidden'); document.getElementById('header-raw-btn').classList.remove('hidden'); document.getElementById('header-reanimator-btn').classList.remove('hidden'); document.getElementById('header-logs-csv-btn').classList.remove('hidden'); @@ -1425,6 +1424,8 @@ async function loadPrivacyScan() { const customer = document.getElementById('privacy-customer'); if (!section || !rows) return; + const clearBtn = document.getElementById('clear-btn'); + let data; try { const resp = await fetch('/api/privacy-scan'); @@ -1436,6 +1437,7 @@ async function loadPrivacyScan() { if (!data || data.loaded === false) { section.classList.add('hidden'); + if (clearBtn) clearBtn.classList.add('hidden'); return; } @@ -1443,6 +1445,13 @@ async function loadPrivacyScan() { const customers = Array.isArray(data.customers) ? data.customers : []; const san = data.sanitized || null; const alreadySanitized = !!(san && san.detected); + + if (clearBtn) { + clearBtn.classList.toggle('hidden', !data.sanitizable); + clearBtn.disabled = alreadySanitized; + clearBtn.textContent = alreadySanitized ? 'Personal data removed' : 'Remove personal data'; + } + if (findings.length === 0 && customers.length === 0 && !alreadySanitized) { section.classList.add('hidden'); return; @@ -1500,67 +1509,30 @@ async function loadPrivacyScan() { if (findingsBox) findingsBox.style.display = privacyCollapsed ? 'none' : ''; } - const sanBox = document.getElementById('privacy-sanitize'); - if (sanBox) { - sanBox.classList.toggle('hidden', !data.sanitizable && !alreadySanitized); - const prev = document.getElementById('privacy-sanitize-preview'); - const btn = document.getElementById('privacy-sanitize-btn'); - if (alreadySanitized) { - if (btn) btn.classList.add('hidden'); - if (prev) { - prev.innerHTML = `
Файл выглядит уже обезличенным: ` + - `${san.markers} маркер(ов) в ${san.files} файл(ах).` + - ((san.evidence && san.evidence.length) - ? `
    ${san.evidence.map(e => `
  • ${escapeHtml(e)}
  • `).join('')}
` - : '') + `
`; - prev.classList.remove('hidden'); - } - } else { - if (btn) { btn.classList.remove('hidden'); btn.disabled = false; btn.textContent = 'Обезличить и скачать копию'; } - if (prev) { prev.classList.add('hidden'); prev.innerHTML = ''; } - } - } - section.classList.remove('hidden'); } -async function sanitizePreview() { - const btn = document.getElementById('privacy-sanitize-btn'); - const prev = document.getElementById('privacy-sanitize-preview'); - if (!btn || !prev) return; +// Header "Remove personal data": sanitize the uploaded file and download the +// clean copy in one click - no preview, no confirmation. +async function removePersonalData() { + const btn = document.getElementById('clear-btn'); + if (!btn || btn.disabled) return; + const label = btn.textContent; btn.disabled = true; - btn.textContent = 'Обработка…'; - let data; + btn.textContent = 'Working…'; try { const resp = await fetch('/api/sanitize', { method: 'POST' }); - data = await resp.json(); - if (!resp.ok) throw new Error(data && data.error ? data.error : 'sanitize failed'); + if (!resp.ok) { + const d = await resp.json().catch(() => ({})); + throw new Error(d.error || ('HTTP ' + resp.status)); + } + window.location = '/api/sanitize/download'; + setTimeout(() => { btn.disabled = false; btn.textContent = label; }, 2000); } catch (e) { - prev.innerHTML = `
${escapeHtml(String(e.message || e))}
`; - prev.classList.remove('hidden'); - btn.disabled = false; - btn.textContent = 'Обезличить и скачать копию'; - return; + console.error('remove personal data failed:', e); + btn.textContent = 'Failed'; + setTimeout(() => { btn.disabled = false; btn.textContent = label; }, 2500); } - - const changes = Array.isArray(data.changes) ? data.changes : []; - const skipped = Array.isArray(data.skipped_binary) ? data.skipped_binary : []; - let html = `

${data.total_replaced} замен в ${changes.length} файл(ах). ` + - `Формат, имена и даты записей сохранены; правки той же длины.

`; - if (changes.length) { - html += '' + - changes.map(c => ``).join('') + - '
КатегорияКол-воФайл
${escapeHtml(c.category)}${c.count}${escapeHtml(c.path)}
'; - } - if (skipped.length) { - html += `
Не удалось отредактировать (обезличьте вручную):
    ` + - skipped.map(s => `
  • ${escapeHtml(s)}
  • `).join('') + '
'; - } - html += ``; - prev.innerHTML = html; - prev.classList.remove('hidden'); - btn.disabled = false; - btn.textContent = 'Пересобрать'; } // The header + customer summary stay visible; only the findings table collapses. @@ -1667,50 +1639,6 @@ function exportData(format) { window.location.href = `/api/export/${format}`; } -// Clear data -async function clearData() { - try { - await fetch('/api/clear', { method: 'DELETE' }); - document.getElementById('upload-section').classList.remove('hidden'); - document.getElementById('data-section').classList.add('hidden'); - document.getElementById('clear-btn').classList.add('hidden'); - document.getElementById('header-raw-btn').classList.add('hidden'); - document.getElementById('header-reanimator-btn').classList.add('hidden'); - document.getElementById('header-logs-csv-btn').classList.add('hidden'); - document.getElementById('header-log-meta').classList.add('hidden'); - document.getElementById('upload-status').textContent = ''; - const frame = document.getElementById('audit-viewer-frame'); - if (frame) { - frame.src = 'about:blank'; - } - const parseErrSection = document.getElementById('parse-errors-section'); - if (parseErrSection) parseErrSection.classList.add('hidden'); - const parseErrRows = document.getElementById('parse-errors-rows'); - if (parseErrRows) parseErrRows.innerHTML = ''; - parseErrorsCollapsed = false; - const parseErrBody = document.getElementById('parse-errors-body'); - if (parseErrBody) parseErrBody.style.display = ''; - const parseErrToggle = document.getElementById('parse-errors-toggle'); - if (parseErrToggle) parseErrToggle.textContent = '▲'; - const privacySection = document.getElementById('privacy-section'); - if (privacySection) privacySection.classList.add('hidden'); - const privacyRows = document.getElementById('privacy-rows'); - if (privacyRows) privacyRows.innerHTML = ''; - const privacyCustomer = document.getElementById('privacy-customer'); - if (privacyCustomer) privacyCustomer.innerHTML = ''; - privacyCollapsed = true; - const privacyFindings = document.getElementById('privacy-findings'); - if (privacyFindings) privacyFindings.style.display = 'none'; - const privacyToggle = document.getElementById('privacy-toggle'); - if (privacyToggle) { privacyToggle.textContent = '▼'; privacyToggle.style.visibility = ''; } - const privacySan = document.getElementById('privacy-sanitize'); - if (privacySan) privacySan.classList.add('hidden'); - const privacySanPrev = document.getElementById('privacy-sanitize-preview'); - if (privacySanPrev) { privacySanPrev.innerHTML = ''; privacySanPrev.classList.add('hidden'); } - } catch (err) { - console.error('Failed to clear data:', err); - } -} // Restart app (reload page) function restartApp() { diff --git a/web/templates/index.html b/web/templates/index.html index a193ea2..8d1fe1a 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -15,7 +15,7 @@