feat: Add anonymisation issue summary rendering for flagged series in upload process

This commit is contained in:
Ross
2026-05-11 12:17:43 +01:00
parent 52e6e79c1b
commit 22c5576fc9
+111
View File
@@ -71,6 +71,7 @@
<div class="alert alert-warning d-none anon-alert" role="alert">
<p class="mb-2">Potential anonymisation issues were detected. Flagged series are highlighted below.</p>
<div id="anon-issue-summary" class="small"></div>
<details>
<summary>Override anonymisation check</summary>
<p class="mt-2 mb-2">Only override if you are certain the warnings are false positives.</p>
@@ -210,6 +211,12 @@
return String(value).trim();
}
function escapeHtml(value) {
const div = document.createElement("div");
div.textContent = value === undefined || value === null ? "" : String(value);
return div.innerHTML;
}
async function parseDicomMetadata(file) {
try {
const buffer = await file.arrayBuffer();
@@ -319,6 +326,108 @@
});
}
function buildAnonymisationIssueSummary() {
const selectedRecords = window.uploadPreview.records.filter((record) => record.include);
if (!selectedRecords.length) {
return null;
}
const seriesMap = {};
let tagWarningFiles = 0;
let ocrWarningFiles = 0;
for (const record of selectedRecords) {
const hasTagWarnings = record.tagWarnings.length > 0;
const hasOcrWarning = !!record.ocrWarning;
if (!hasTagWarnings && !hasOcrWarning) {
continue;
}
if (!seriesMap[record.seriesUid]) {
seriesMap[record.seriesUid] = {
uid: record.seriesUid,
description: record.seriesDescription || "(no series description)",
tagFiles: 0,
ocrFiles: 0,
details: new Set(),
};
}
const entry = seriesMap[record.seriesUid];
if (hasTagWarnings) {
tagWarningFiles += 1;
entry.tagFiles += 1;
record.tagWarnings.forEach((warning) => entry.details.add(warning));
}
if (hasOcrWarning) {
ocrWarningFiles += 1;
entry.ocrFiles += 1;
entry.details.add("Possible burned-in patient text detected by OCR");
}
}
const seriesIssues = Object.values(seriesMap);
if (!seriesIssues.length) {
return null;
}
seriesIssues.sort((a, b) => {
const aScore = a.tagFiles + a.ocrFiles;
const bScore = b.tagFiles + b.ocrFiles;
return bScore - aScore;
});
return {
selectedFiles: selectedRecords.length,
flaggedSeries: seriesIssues.length,
tagWarningFiles,
ocrWarningFiles,
issues: seriesIssues,
};
}
function renderAnonymisationIssueSummary() {
const summary = document.getElementById("anon-issue-summary");
if (!summary) return;
const data = buildAnonymisationIssueSummary();
if (!data) {
summary.innerHTML = "";
return;
}
const issueItems = data.issues.slice(0, 8).map((issue) => {
const issueBits = [];
if (issue.tagFiles) issueBits.push(`Tag warnings in ${issue.tagFiles} file${issue.tagFiles === 1 ? "" : "s"}`);
if (issue.ocrFiles) issueBits.push(`OCR warnings in ${issue.ocrFiles} file${issue.ocrFiles === 1 ? "" : "s"}`);
const detailPreview = Array.from(issue.details).slice(0, 2).join("; ");
return `
<li>
<strong>${escapeHtml(issue.description)}</strong>
<span class="text-muted">(${escapeHtml(issue.uid)})</span><br>
${escapeHtml(issueBits.join(" \u00b7 "))}
${detailPreview ? `<br><span class="text-muted">${escapeHtml(detailPreview)}</span>` : ""}
</li>
`;
}).join("");
const extraCount = Math.max(0, data.issues.length - 8);
const extraLine = extraCount ? `<p class="mb-0 mt-2 text-muted">+ ${extraCount} more flagged series</p>` : "";
summary.innerHTML = `
<p class="mb-2">
<strong>Summary:</strong>
${data.flaggedSeries} flagged series across ${data.selectedFiles} selected files.
Tag warnings: ${data.tagWarningFiles}. OCR warnings: ${data.ocrWarningFiles}.
</p>
<ul class="mb-0 ps-3">
${issueItems}
</ul>
${extraLine}
`;
}
function updateUploadButtonState() {
const uploadButton = document.getElementById("uploadButton");
const anonAlert = document.querySelector(".anon-alert");
@@ -329,6 +438,8 @@
const hasPendingDuplicateChecks = duplicatesEnabled && hasSelected &&
window.uploadPreview.records.some((r) => r.include && !r.duplicateChecked);
renderAnonymisationIssueSummary();
if (!hasSelected) {
uploadButton.disabled = true;
anonAlert.classList.add("d-none");