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 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-09-03 10:19:44 +03:00
co-authored by Claude Sonnet 5
parent 21670801f3
commit 95f552a0e5
3 changed files with 27 additions and 160 deletions
-57
View File
@@ -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;
}
+26 -98
View File
@@ -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 = `<div class="privacy-sanitized-note">Файл выглядит уже обезличенным: ` +
`${san.markers} маркер(ов) в ${san.files} файл(ах).` +
((san.evidence && san.evidence.length)
? `<ul class="privacy-evidence">${san.evidence.map(e => `<li>${escapeHtml(e)}</li>`).join('')}</ul>`
: '') + `</div>`;
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 = `<div class="privacy-sanitize-err">${escapeHtml(String(e.message || e))}</div>`;
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 = `<p><strong>${data.total_replaced}</strong> замен в <strong>${changes.length}</strong> файл(ах). ` +
`Формат, имена и даты записей сохранены; правки той же длины.</p>`;
if (changes.length) {
html += '<table class="parse-errors-table"><thead><tr><th>Категория</th><th>Кол-во</th><th>Файл</th></tr></thead><tbody>' +
changes.map(c => `<tr><td>${escapeHtml(c.category)}</td><td>${c.count}</td><td>${escapeHtml(c.path)}</td></tr>`).join('') +
'</tbody></table>';
}
if (skipped.length) {
html += `<div class="privacy-sanitize-warn"><strong>Не удалось отредактировать (обезличьте вручную):</strong><ul>` +
skipped.map(s => `<li>${escapeHtml(s)}</li>`).join('') + '</ul></div>';
}
html += `<button type="button" class="privacy-sanitize-dl" onclick="window.location='/api/sanitize/download'">Скачать обезличенную копию</button>`;
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() {