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:
co-authored by
Claude Sonnet 5
parent
21670801f3
commit
95f552a0e5
@@ -1047,60 +1047,3 @@ code {
|
|||||||
color: var(--muted);
|
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
@@ -1407,7 +1407,6 @@ let auditViewerNonce = 0;
|
|||||||
async function loadData(vendor, filename) {
|
async function loadData(vendor, filename) {
|
||||||
document.getElementById('upload-section').classList.add('hidden');
|
document.getElementById('upload-section').classList.add('hidden');
|
||||||
document.getElementById('data-section').classList.remove('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-raw-btn').classList.remove('hidden');
|
||||||
document.getElementById('header-reanimator-btn').classList.remove('hidden');
|
document.getElementById('header-reanimator-btn').classList.remove('hidden');
|
||||||
document.getElementById('header-logs-csv-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');
|
const customer = document.getElementById('privacy-customer');
|
||||||
if (!section || !rows) return;
|
if (!section || !rows) return;
|
||||||
|
|
||||||
|
const clearBtn = document.getElementById('clear-btn');
|
||||||
|
|
||||||
let data;
|
let data;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/api/privacy-scan');
|
const resp = await fetch('/api/privacy-scan');
|
||||||
@@ -1436,6 +1437,7 @@ async function loadPrivacyScan() {
|
|||||||
|
|
||||||
if (!data || data.loaded === false) {
|
if (!data || data.loaded === false) {
|
||||||
section.classList.add('hidden');
|
section.classList.add('hidden');
|
||||||
|
if (clearBtn) clearBtn.classList.add('hidden');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1443,6 +1445,13 @@ async function loadPrivacyScan() {
|
|||||||
const customers = Array.isArray(data.customers) ? data.customers : [];
|
const customers = Array.isArray(data.customers) ? data.customers : [];
|
||||||
const san = data.sanitized || null;
|
const san = data.sanitized || null;
|
||||||
const alreadySanitized = !!(san && san.detected);
|
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) {
|
if (findings.length === 0 && customers.length === 0 && !alreadySanitized) {
|
||||||
section.classList.add('hidden');
|
section.classList.add('hidden');
|
||||||
return;
|
return;
|
||||||
@@ -1500,67 +1509,30 @@ async function loadPrivacyScan() {
|
|||||||
if (findingsBox) findingsBox.style.display = privacyCollapsed ? 'none' : '';
|
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');
|
section.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sanitizePreview() {
|
// Header "Remove personal data": sanitize the uploaded file and download the
|
||||||
const btn = document.getElementById('privacy-sanitize-btn');
|
// clean copy in one click - no preview, no confirmation.
|
||||||
const prev = document.getElementById('privacy-sanitize-preview');
|
async function removePersonalData() {
|
||||||
if (!btn || !prev) return;
|
const btn = document.getElementById('clear-btn');
|
||||||
|
if (!btn || btn.disabled) return;
|
||||||
|
const label = btn.textContent;
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.textContent = 'Обработка…';
|
btn.textContent = 'Working…';
|
||||||
let data;
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/api/sanitize', { method: 'POST' });
|
const resp = await fetch('/api/sanitize', { method: 'POST' });
|
||||||
data = await resp.json();
|
if (!resp.ok) {
|
||||||
if (!resp.ok) throw new Error(data && data.error ? data.error : 'sanitize failed');
|
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) {
|
} catch (e) {
|
||||||
prev.innerHTML = `<div class="privacy-sanitize-err">${escapeHtml(String(e.message || e))}</div>`;
|
console.error('remove personal data failed:', e);
|
||||||
prev.classList.remove('hidden');
|
btn.textContent = 'Failed';
|
||||||
btn.disabled = false;
|
setTimeout(() => { btn.disabled = false; btn.textContent = label; }, 2500);
|
||||||
btn.textContent = 'Обезличить и скачать копию';
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
// The header + customer summary stay visible; only the findings table collapses.
|
||||||
@@ -1667,50 +1639,6 @@ function exportData(format) {
|
|||||||
window.location.href = `/api/export/${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)
|
// Restart app (reload page)
|
||||||
function restartApp() {
|
function restartApp() {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="header-log-meta" class="header-log-meta hidden">
|
<div id="header-log-meta" class="header-log-meta hidden">
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<button id="clear-btn" class="header-action hidden" onclick="clearData()">Clear Data</button>
|
<button id="clear-btn" class="header-action hidden" onclick="removePersonalData()">Remove personal data</button>
|
||||||
<button id="header-raw-btn" class="header-action hidden" onclick="exportData('json')">Raw Data</button>
|
<button id="header-raw-btn" class="header-action hidden" onclick="exportData('json')">Raw Data</button>
|
||||||
<button id="header-reanimator-btn" class="header-action hidden" onclick="exportData('reanimator')">Reanimator</button>
|
<button id="header-reanimator-btn" class="header-action hidden" onclick="exportData('reanimator')">Reanimator</button>
|
||||||
<button id="header-logs-csv-btn" class="header-action hidden" onclick="exportData('logs-csv')">Logs Export</button>
|
<button id="header-logs-csv-btn" class="header-action hidden" onclick="exportData('logs-csv')">Logs Export</button>
|
||||||
@@ -166,10 +166,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="privacy-body" class="parse-errors-body">
|
<div id="privacy-body" class="parse-errors-body">
|
||||||
<div id="privacy-customer" class="privacy-customer"></div>
|
<div id="privacy-customer" class="privacy-customer"></div>
|
||||||
<div id="privacy-sanitize" class="privacy-sanitize hidden">
|
|
||||||
<button id="privacy-sanitize-btn" type="button" onclick="sanitizePreview()">Обезличить и скачать копию</button>
|
|
||||||
<div id="privacy-sanitize-preview" class="privacy-sanitize-preview hidden"></div>
|
|
||||||
</div>
|
|
||||||
<div id="privacy-findings" class="privacy-findings" style="display:none">
|
<div id="privacy-findings" class="privacy-findings" style="display:none">
|
||||||
<table class="parse-errors-table">
|
<table class="parse-errors-table">
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
Reference in New Issue
Block a user