Built-in file redaction for local files
This commit is contained in:
1 parent
c490b3d76a
commit
23b9555dcf
11 files changed
+576
-20
No files matched your search
+27
-1
@@ -82,6 +82,31 @@ async function loadHistorySession(refScanId) {
|
||||
try { window.markOverdueCards(); } catch(_) {}
|
||||
try { window.loadTrend(); } catch(_) {}
|
||||
_setHistoryBanner(true, resolvedRef);
|
||||
|
||||
// ── Re-scan diff: append items from previous session no longer present ────
|
||||
const allSessions = _sessions !== null ? _sessions : await _fetchSessions();
|
||||
const idx = allSessions.findIndex(s => s.ref_scan_id === resolvedRef);
|
||||
if (idx !== -1 && idx + 1 < allSessions.length) {
|
||||
const prevRef = allSessions[idx + 1].ref_scan_id;
|
||||
try {
|
||||
const pr = await fetch('/api/db/flagged?ref=' + prevRef);
|
||||
const prevItems = await pr.json();
|
||||
if (Array.isArray(prevItems) && prevItems.length) {
|
||||
const currentIds = new Set(items.map(f => f.id));
|
||||
const resolved = prevItems.filter(f => !currentIds.has(f.id));
|
||||
if (resolved.length) {
|
||||
const divider = document.createElement('div');
|
||||
divider.className = 'resolved-divider';
|
||||
divider.textContent = resolved.length + ' ' + t('history_resolved_label', 'items no longer present');
|
||||
document.getElementById('grid')?.appendChild(divider);
|
||||
resolved.forEach(f => { f._resolved = true; window.appendCard(f); });
|
||||
_setHistoryBanner(true, resolvedRef, resolved.length);
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
console.warn('[history] diff failed:', e);
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
console.error('[history] failed to load session:', e);
|
||||
}
|
||||
@@ -89,7 +114,7 @@ async function loadHistorySession(refScanId) {
|
||||
|
||||
// ── Banner ────────────────────────────────────────────────────────────────────
|
||||
|
||||
function _setHistoryBanner(visible, resolvedRef) {
|
||||
function _setHistoryBanner(visible, resolvedRef, resolvedCount) {
|
||||
const banner = document.getElementById('historyBanner');
|
||||
const bannerTxt = document.getElementById('historyBannerText');
|
||||
const latestBtn = document.getElementById('historyLatestBtn');
|
||||
@@ -107,6 +132,7 @@ function _setHistoryBanner(visible, resolvedRef) {
|
||||
label = date + ' ' + time
|
||||
+ (srcStr ? ' · ' + srcStr : '')
|
||||
+ ' · ' + sess.flagged_count + ' ' + t('history_items', 'items');
|
||||
if (resolvedCount) label += ' · ' + resolvedCount + ' ' + t('history_resolved_badge', 'resolved');
|
||||
} else {
|
||||
label = S.flaggedData.length + ' ' + t('history_items', 'items');
|
||||
}
|
||||
|
||||
+37
-6
@@ -24,7 +24,7 @@ function appendCard(f) {
|
||||
: '/api/thumb?name=' + encodeURIComponent(f.name) + '&type=' + encodeURIComponent(f.source_type);
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card' + (S.isListView ? ' list-view' : '') + (S._selectedIds.has(f.id) ? ' card-selected-bulk' : '');
|
||||
card.className = 'card' + (S.isListView ? ' list-view' : '') + (S._selectedIds.has(f.id) ? ' card-selected-bulk' : '') + (f._resolved ? ' card-resolved' : '');
|
||||
card.dataset.id = f.id;
|
||||
card.onclick = (e) => { if (S._selectMode) { toggleCardSelect(f.id, e); } else { openPreview(f); } };
|
||||
|
||||
@@ -35,7 +35,11 @@ function appendCard(f) {
|
||||
cb.onclick = (e) => { e.stopPropagation(); toggleCardSelect(f.id, e); };
|
||||
card.appendChild(cb);
|
||||
|
||||
const delBtn = window.VIEWER_MODE ? '' : `<button class="card-delete-btn" title="${t('m365_delete_confirm','Delete')}" onclick="event.stopPropagation();deleteItem(${JSON.stringify(f).replace(/"/g,'"')},this.closest('.card'))">🗑</button>`;
|
||||
const delBtn = (window.VIEWER_MODE || f._resolved) ? '' : `<button class="card-delete-btn" title="${t('m365_delete_confirm','Delete')}" onclick="event.stopPropagation();deleteItem(${JSON.stringify(f).replace(/"/g,'"')},this.closest('.card'))">🗑</button>`;
|
||||
const _redactExts = new Set(['.docx', '.xlsx', '.txt', '.csv']);
|
||||
const _redactable = !window.VIEWER_MODE && !f._resolved && f.source_type === 'local' && f.cpr_count > 0
|
||||
&& _redactExts.has((f.name || '').substring((f.name || '').lastIndexOf('.')).toLowerCase());
|
||||
const redactBtn = _redactable ? `<button class="card-redact-btn" title="${t('redact_btn','Redact CPR')}" onclick="event.stopPropagation();redactItem(${JSON.stringify(f).replace(/"/g,'"')},this.closest('.card'))">✏</button>` : '';
|
||||
|
||||
if (S.isListView) {
|
||||
card.innerHTML = `
|
||||
@@ -50,8 +54,8 @@ function appendCard(f) {
|
||||
${f.phone_count > 0 ? '<span class="phone-badge">' + f.phone_count + ' ' + t('m365_badge_phones', 'tlf.') + '</span> ' : ''}
|
||||
${f.face_count > 0 ? '<span class="photo-face-badge">' + f.face_count + ' ' + t('m365_badge_faces', f.face_count === 1 ? 'face' : 'faces') + '</span> ' : ''}
|
||||
${f.exif && f.exif.gps ? '<span class="photo-face-badge" style="background:#0a3a5a;color:#7ec8d0">🌍 GPS</span> ' : ''}
|
||||
${f.special_category && f.special_category.length ? '<span class="special-cat-badge">⚠ Art.9 — ' + f.special_category.filter(function(s){return s !== 'gps_location' && s !== 'exif_pii';}).join(', ') + '</span> ' : ''}${f.overdue ? '<span class="overdue-badge">🗓 Overdue</span>' : ''}
|
||||
${delBtn}`;
|
||||
${f.special_category && f.special_category.length ? '<span class="special-cat-badge">⚠ Art.9 — ' + f.special_category.filter(function(s){return s !== 'gps_location' && s !== 'exif_pii';}).join(', ') + '</span> ' : ''}${f._resolved ? '<span class="resolved-badge">✓ ' + t('history_resolved_badge', 'Resolved') + '</span> ' : ''}${f.overdue ? '<span class="overdue-badge">🗓 Overdue</span>' : ''}
|
||||
${delBtn}${redactBtn}`;
|
||||
} else {
|
||||
card.innerHTML = `
|
||||
<div class="thumb-wrap"><img src="${src}" alt="${f.name}" loading="lazy"></div>
|
||||
@@ -60,9 +64,9 @@ function appendCard(f) {
|
||||
<div class="card-meta">${f.size_kb} KB · ${f.modified || ''}</div>
|
||||
${f.folder ? `<div class="card-meta" style="font-size:10px" title="${f.folder}">📂 ${f.folder}</div>` : ''}
|
||||
<div class="card-source"><span class="source-badge ${badgeCls}">${label}</span>${f.account_name ? ' <span class="account-pill" title="' + f.account_name + '">' + (f.user_role === "student" ? '<span class="role-badge">' + t("role_student","Elev") + "</span>" : f.user_role === "staff" ? '<span class="role-badge">' + t("role_staff","Ansat") + "</span>" : "") + f.account_name + '</span>' : ''}${f.transfer_risk === "external-recipient" ? ' <span class="role-pill" style="background:#7B2D00;color:#FFD0B0">⚠ Ext.</span>' : f.transfer_risk ? ' <span class="role-pill" style="background:#003D7B;color:#B0D4FF">🔗</span>' : ''}</div>
|
||||
<span class="cpr-badge">${f.cpr_count} CPR</span>${f.email_count > 0 ? ' <span class="email-badge">' + f.email_count + ' ' + t('m365_badge_emails', 'e-mail') + '</span>' : ''}${f.phone_count > 0 ? ' <span class="phone-badge">' + f.phone_count + ' ' + t('m365_badge_phones', 'tlf.') + '</span>' : ''}${f.face_count > 0 ? ' <span class="photo-face-badge">' + f.face_count + ' ' + t('m365_badge_faces', f.face_count === 1 ? 'face' : 'faces') + '</span>' : ''}${f.exif && f.exif.gps ? ' <span class="photo-face-badge" style="background:#0a3a5a;color:#7ec8d0">🌍 GPS</span>' : ''}${f.overdue ? ' <span class="overdue-badge">🗓 Overdue</span>' : ''}
|
||||
<span class="cpr-badge">${f.cpr_count} CPR</span>${f.email_count > 0 ? ' <span class="email-badge">' + f.email_count + ' ' + t('m365_badge_emails', 'e-mail') + '</span>' : ''}${f.phone_count > 0 ? ' <span class="phone-badge">' + f.phone_count + ' ' + t('m365_badge_phones', 'tlf.') + '</span>' : ''}${f.face_count > 0 ? ' <span class="photo-face-badge">' + f.face_count + ' ' + t('m365_badge_faces', f.face_count === 1 ? 'face' : 'faces') + '</span>' : ''}${f.exif && f.exif.gps ? ' <span class="photo-face-badge" style="background:#0a3a5a;color:#7ec8d0">🌍 GPS</span>' : ''}${f._resolved ? ' <span class="resolved-badge">✓ ' + t('history_resolved_badge', 'Resolved') + '</span>' : ''}${f.overdue ? ' <span class="overdue-badge">🗓 Overdue</span>' : ''}
|
||||
</div>
|
||||
${delBtn}`;
|
||||
${delBtn}${redactBtn}`;
|
||||
}
|
||||
grid.appendChild(card);
|
||||
}
|
||||
@@ -594,6 +598,32 @@ async function deleteItem(f, cardEl) {
|
||||
}
|
||||
}
|
||||
|
||||
async function redactItem(f, cardEl) {
|
||||
if (!confirm(t('redact_confirm', 'Redact all CPR numbers in') + ' "' + f.name + '"?\n\n' + t('redact_warning', 'CPR numbers will be replaced with █ characters. This cannot be undone.'))) return;
|
||||
if (cardEl) { cardEl.style.opacity = '0.5'; cardEl.style.pointerEvents = 'none'; }
|
||||
try {
|
||||
const r = await fetch('/api/redact_item', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({id: f.id, source_type: f.source_type})
|
||||
});
|
||||
const d = await r.json();
|
||||
if (d.ok) {
|
||||
S.flaggedData = S.flaggedData.filter(x => x.id !== f.id);
|
||||
S.filteredData = S.filteredData.filter(x => x.id !== f.id);
|
||||
if (cardEl) cardEl.remove();
|
||||
updateStats();
|
||||
log(t('redact_done', 'Redacted') + ' ' + f.name + ' (' + (d.redacted || 0) + ' ' + t('redact_spans', 'CPR spans') + ')', 'ok');
|
||||
if (_previewItemId === f.id) closePreview();
|
||||
} else {
|
||||
if (cardEl) { cardEl.style.opacity = ''; cardEl.style.pointerEvents = ''; }
|
||||
log(t('redact_failed', 'Redaction failed:') + ' ' + (d.error || '?'), 'err');
|
||||
}
|
||||
} catch(e) {
|
||||
if (cardEl) { cardEl.style.opacity = ''; cardEl.style.pointerEvents = ''; }
|
||||
log(t('redact_failed', 'Redaction failed:') + ' ' + e.message, 'err');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Bulk delete modal ─────────────────────────────────────────────────────────
|
||||
|
||||
function openBulkDelete() {
|
||||
@@ -1049,6 +1079,7 @@ window.loadDisposition = loadDisposition;
|
||||
window.saveDisposition = saveDisposition;
|
||||
window.closePreview = closePreview;
|
||||
window.deleteItem = deleteItem;
|
||||
window.redactItem = redactItem;
|
||||
window.openBulkDelete = openBulkDelete;
|
||||
window.closeBulkDelete = closeBulkDelete;
|
||||
window._bdFilters = _bdFilters;
|
||||
|
||||
@@ -253,6 +253,9 @@
|
||||
.card-delete-btn { position:absolute; top:6px; right:6px; background:rgba(0,0,0,0.45); color:#fff; border:none; border-radius:50%; width:22px; height:22px; font-size:13px; line-height:22px; text-align:center; cursor:pointer; opacity:0.35; transition:opacity .15s; padding:0; z-index:1; }
|
||||
.card:hover .card-delete-btn { opacity:1; }
|
||||
.card.list-view .card-delete-btn { position:static; opacity:1; background:transparent; color:var(--muted); flex-shrink:0; }
|
||||
.card-redact-btn { position:absolute; top:6px; right:32px; background:rgba(0,80,40,0.55); color:#7effc0; border:none; border-radius:50%; width:22px; height:22px; font-size:12px; line-height:22px; text-align:center; cursor:pointer; opacity:0; transition:opacity .15s; padding:0; z-index:1; }
|
||||
.card:hover .card-redact-btn { opacity:1; }
|
||||
.card.list-view .card-redact-btn { position:static; opacity:1; background:transparent; color:#7effc0; flex-shrink:0; }
|
||||
|
||||
/* Per-card checkbox (select mode) */
|
||||
.card-cb { position:absolute; top:6px; left:6px; width:16px; height:16px; margin:0; cursor:pointer; z-index:2;
|
||||
@@ -491,6 +494,12 @@
|
||||
.overdue-badge { font-size: 9px; padding: 1px 5px; border-radius: 10px;
|
||||
background: #7c3200; color: #ffb347; font-weight: 600; white-space: nowrap; }
|
||||
[data-theme="light"] .overdue-badge { background: #fff3e0; color: #c55a00; }
|
||||
.resolved-badge { font-size: 9px; padding: 1px 5px; border-radius: 10px;
|
||||
background: #1a3a28; color: #7effc0; font-weight: 600; white-space: nowrap; }
|
||||
[data-theme="light"] .resolved-badge { background: #d0f5ea; color: #005a3a; }
|
||||
.card-resolved { opacity: 0.6; }
|
||||
.resolved-divider { grid-column: 1 / -1; padding: 8px 2px; font-size: 11px;
|
||||
color: var(--muted); border-top: 1px dashed var(--border); text-align: center; }
|
||||
.email-badge { font-size: 9px; padding: 1px 5px; border-radius: 10px;
|
||||
background: #1a3a5c; color: #7ec8f0; font-weight: 500; white-space: nowrap; }
|
||||
[data-theme="light"] .email-badge { background: #d0eaff; color: #004a80; }
|
||||
|
||||
Reference in new issue
Block a user