/* Results display — score, stats, issues, filters, remediation */
let currentFilter = 'all';
let allIssues = [];
let dismissedIndices = new Set();
let overriddenChecks = new Set();
let scoreBreakdownData = null;
let originalSeverityCounts = null;
let lastMatterhornData = null;
// WCAG 2.1 criterion → conformance level (mirrors enterprise_pdf_checker.py)
const WCAG_LEVELS = {
'1.1.1':'A','1.2.1':'A','1.2.2':'A','1.2.3':'A',
'1.2.4':'AA','1.2.5':'AA',
'1.3.1':'A','1.3.2':'A','1.3.3':'A',
'1.3.4':'AA','1.3.5':'AA',
'1.4.1':'A','1.4.2':'A',
'1.4.3':'AA','1.4.4':'AA','1.4.5':'AA',
'1.4.10':'AA','1.4.11':'AA','1.4.12':'AA','1.4.13':'AA',
'2.1.1':'A','2.1.2':'A','2.1.4':'A',
'2.2.1':'A','2.2.2':'A',
'2.3.1':'A',
'2.4.1':'A','2.4.2':'A','2.4.3':'A','2.4.4':'A',
'2.4.5':'AA','2.4.6':'AA','2.4.7':'AA',
'2.5.1':'A','2.5.2':'A','2.5.3':'A','2.5.4':'A',
'3.1.1':'A','3.1.2':'AA',
'3.2.1':'A','3.2.2':'A','3.2.3':'AA','3.2.4':'AA',
'3.3.1':'A','3.3.2':'A','3.3.3':'AA','3.3.4':'AA',
'4.1.1':'A','4.1.2':'A','4.1.3':'AA',
};
function displayResults(data) {
document.getElementById('uploadSection').style.display = 'none';
const resultsSection = document.getElementById('resultsSection');
resultsSection.style.display = 'block';
resultsSection.setAttribute('tabindex', '-1');
resultsSection.focus();
document.getElementById('scoreNumber').textContent = data.accessibility_score;
const statsGrid = document.getElementById('statsGrid');
const sc = data.severity_counts;
statsGrid.innerHTML = `
`;
allIssues = data.issues;
dismissedIndices = new Set(data.dismissed_indices || []);
overriddenChecks = new Set(data.overridden_checks || []);
scoreBreakdownData = data.score_breakdown;
originalSeverityCounts = Object.assign({}, data.severity_counts);
displayWcagCompliance(data.wcag_compliance);
displayNextSteps(data.next_steps);
displayScoreBreakdown(data.score_breakdown);
renderRecalcButton();
displayIssues(allIssues);
// If this result was previously adjusted, restore the adjusted view without saving again
if (data.score_breakdown?.adjusted && (dismissedIndices.size > 0 || overriddenChecks.size > 0)) {
applyScoreRecalc();
}
initializePageViewer(data);
displayRemediationOptions(data);
lastMatterhornData = data.matterhorn_summary || null;
displayMatterhorn(lastMatterhornData);
// Refresh history so the new result appears in the table
if (typeof loadHistory === 'function') loadHistory();
}
function displayIssues(issues) {
const issuesList = document.getElementById('issuesList');
if (issues.length === 0) {
issuesList.innerHTML = 'No issues to display
';
return;
}
const pageGroups = {};
const documentWide = [];
issues.forEach(issue => {
if (issue.page_number) {
if (!pageGroups[issue.page_number]) pageGroups[issue.page_number] = [];
pageGroups[issue.page_number].push(issue);
} else {
documentWide.push(issue);
}
});
// Assign issue numbers for coordinate-based issues
let counter = 0;
const issueNumberMap = new Map();
issues.forEach(issue => {
if (issue.coordinates && issue.page_number) {
counter++;
issueNumberMap.set(issue, counter);
}
});
const pageNumbers = Object.keys(pageGroups).map(Number).sort((a, b) => a - b);
// Page overview
let html = '';
if (pageNumbers.length > 0) {
html += '';
html += '
Page Overview ';
html += '
';
pageNumbers.forEach(pn => {
const pi = pageGroups[pn];
const crit = pi.filter(i => i.severity === 'CRITICAL').length;
const err = pi.filter(i => i.severity === 'ERROR').length;
const warn = pi.filter(i => i.severity === 'WARNING').length;
let bg = '#10b981';
if (crit > 0) bg = '#dc2626'; else if (err > 0) bg = '#ef4444'; else if (warn > 0) bg = '#f59e0b';
html += `
Page
${pn}
${pi.length} issue${pi.length !== 1 ? 's' : ''}
`;
});
html += '
';
}
// Document-wide issues — group table issues by sub-type
if (documentWide.length > 0) {
const tableIssues = documentWide.filter(i => i.category === 'Tables' && !i.page_number);
const otherIssues = documentWide.filter(i => !(i.category === 'Tables' && !i.page_number));
// Group table issues: scope warnings vs caption infos
const tableGroups = {};
tableIssues.forEach(issue => {
const desc = issue.description || '';
const key = desc.includes('scope') ? 'scope'
: desc.includes('Caption') ? 'caption'
: desc.includes('header') ? 'header'
: 'other';
if (!tableGroups[key]) tableGroups[key] = [];
tableGroups[key].push(issue);
});
const groupLabels = { scope: 'Table Scope Issues', caption: 'Table Caption Issues', header: 'Table Header Issues', other: 'Table Issues' };
const groupSeverity = { scope: 'WARNING', caption: 'INFO', header: 'ERROR', other: 'WARNING' };
let tableGroupHtml = '';
Object.entries(tableGroups).forEach(([key, groupIssues]) => {
if (!groupIssues.length) return;
const groupIndices = groupIssues.map(i => allIssues.indexOf(i));
const allDismissed = groupIndices.every(idx => dismissedIndices.has(idx));
const label = groupLabels[key];
const sev = groupSeverity[key];
const groupId = `table-group-${key}`;
tableGroupHtml += `
${groupIssues.map(i => createIssueCard(i, issueNumberMap.get(i), allIssues.indexOf(i))).join('')}
`;
});
const visibleCount = otherIssues.length + Object.keys(tableGroups).length;
html += `
Document-Wide Issues (${visibleCount}) ▼
${tableGroupHtml}
${otherIssues.map(i => createIssueCard(i, issueNumberMap.get(i), allIssues.indexOf(i))).join('')}
`;
}
// Page-specific issues
pageNumbers.forEach(pn => {
const pi = pageGroups[pn];
const crit = pi.filter(i => i.severity === 'CRITICAL').length;
const err = pi.filter(i => i.severity === 'ERROR').length;
const warn = pi.filter(i => i.severity === 'WARNING').length;
html += `
Page ${pn} - ${pi.length} Issue${pi.length !== 1 ? 's' : ''}
${crit > 0 ? `${crit} Critical ` : ''}
${err > 0 ? `${err} Error${err !== 1 ? 's' : ''} ` : ''}
${warn > 0 ? `${warn} Warning${warn !== 1 ? 's' : ''} ` : ''}
▼
${pi.map(i => createIssueCard(i, issueNumberMap.get(i), allIssues.indexOf(i))).join('')}
`;
});
issuesList.innerHTML = html;
}
function createIssueCard(issue, issueNumber, globalIndex) {
const icon = getSeverityIcon(issue.severity);
const catIcon = getCategoryIcon(issue.category);
const isDismissed = dismissedIndices.has(globalIndex);
const markerBadge = issue.coordinates && issueNumber !== undefined
? `📍 #${issueNumber} `
: '';
const dismissBtn = isDismissed
? `↩ Restore `
: `✕ False Positive `;
return `
${issue.description}
${issue.wcag_criterion ? `
${wcagCriterionLinks(issue.wcag_criterion)}
${issue.wcag_level ? `${issue.wcag_level} ` : ''}
` : ''}
${issue.recommendation ? `
Tip: ${issue.recommendation}
` : ''}
`;
}
function togglePageSection(pageNum) {
const section = document.getElementById(`section-${pageNum}`);
const toggle = document.getElementById(`toggle-${pageNum}`);
const header = toggle.closest('h3');
if (section.style.display === 'none') {
section.style.display = 'grid';
toggle.innerHTML = '▼';
if (header) header.setAttribute('aria-expanded', 'true');
} else {
section.style.display = 'none';
toggle.innerHTML = '▶';
if (header) header.setAttribute('aria-expanded', 'false');
}
}
function toggleGroupDetails(groupId) {
const section = document.getElementById(groupId);
const toggle = document.getElementById(`toggle-${groupId}`);
if (!section) return;
if (section.style.display === 'none') {
section.style.display = 'block';
if (toggle) toggle.innerHTML = '▼';
} else {
section.style.display = 'none';
if (toggle) toggle.innerHTML = '▶';
}
}
function dismissIssueGroup(indices) {
indices.forEach(idx => {
if (!dismissedIndices.has(idx)) dismissIssue(idx);
});
}
function scrollToPage(pageNum) {
const el = document.getElementById(`page-${pageNum}`);
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
el.style.background = 'var(--accent-subtle)';
setTimeout(() => { el.style.background = ''; }, 1000);
}
}
function filterIssues(severity) {
currentFilter = severity;
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.classList.remove('active');
btn.setAttribute('aria-pressed', 'false');
});
if (event && event.target) {
event.target.classList.add('active');
event.target.setAttribute('aria-pressed', 'true');
}
const filtered = severity === 'all' ? allIssues : allIssues.filter(i => i.severity === severity);
displayIssues(filtered);
}
/* Remediation */
function displayRemediationOptions(data) {
if (!data.remediation_suggestions || data.auto_fixable_count === 0) return;
document.getElementById('remediationCard').style.display = 'block';
document.getElementById('fixableCount').textContent = data.auto_fixable_count;
const fixesList = document.getElementById('fixesList');
let html = '';
for (const [, fixes] of Object.entries(data.remediation_suggestions)) {
fixes.filter(f => f.auto_fixable).forEach(fix => {
const ic = { ERROR: '\u274C', WARNING: '\u26A0\uFE0F', INFO: '\u2139\uFE0F', CRITICAL: '\u{1F6A8}' };
html += `
${ic[fix.severity] || '\u{1F527}'}
${fix.description}
Will set: ${fix.suggestion}
`;
});
}
html += '
';
fixesList.innerHTML = html;
}
async function applyFixes() {
const btn = document.getElementById('applyFixesBtn');
const resultDiv = document.getElementById('fixResult');
btn.disabled = true;
btn.innerHTML = ' Applying fixes...';
resultDiv.style.display = 'block';
resultDiv.innerHTML = 'Applying automatic fixes to PDF...
';
try {
const result = await remediatePdf(currentJobId);
if (result.success) {
resultDiv.innerHTML = `
${result.data.fixes_applied} issue(s) automatically fixed!
Your remediated PDF is ready for download.
Download Fixed PDF
Filename: ${result.data.original_filename.replace('.pdf', '_fixed.pdf')}
`;
btn.style.display = 'none';
} else {
resultDiv.innerHTML = `
Remediation failed
${result.error}
`;
btn.disabled = false;
btn.innerHTML = 'Retry Auto-Fix ';
}
} catch (error) {
resultDiv.innerHTML = ``;
btn.disabled = false;
btn.innerHTML = 'Retry Auto-Fix ';
}
}
function viewOnPage(pageNum, markerNum) {
const card = document.getElementById('pageViewerCard');
if (card) {
card.style.display = 'block';
card.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
loadVisualPage(pageNum, markerNum);
}
function displayWcagCompliance(compliance) {
const el = document.getElementById('wcagCompliance');
if (!el || !compliance) return;
const levelA = compliance.level_a;
const levelAA = compliance.level_aa;
const aFailures = (compliance.level_a_failures || []).join(', ');
const aaFailures = (compliance.level_aa_failures || []).join(', ');
el.innerHTML = `
WCAG 2.1 A
${levelA ? '✓ Pass' : '✗ Fail'}
WCAG 2.1 AA
${levelAA ? '✓ Pass' : '✗ Fail'}
${!levelA && aFailures ? `Level A failing criteria: ${aFailures}
` : ''}
${!levelAA && !levelA && aaFailures ? `Level AA failing criteria: ${aaFailures}
` : ''}
`;
el.style.display = 'block';
}
function displayNextSteps(steps) {
const el = document.getElementById('nextStepsCard');
const list = document.getElementById('nextStepsList');
if (!el || !list || !steps || steps.length === 0) return;
const priorityLabel = { 1: 'Critical', 2: 'Error', 3: 'Warning' };
const priorityClass = { 1: 'CRITICAL', 2: 'ERROR', 3: 'WARNING' };
list.innerHTML = steps.map((s, i) => `
${i + 1}
${s.action}
${priorityLabel[s.priority] || ''}
${s.category}
${s.wcag ? `${wcagCriterionLinks(s.wcag)} ` : ''}
${s.wcag_level ? `${s.wcag_level} ` : ''}
`).join('');
el.style.display = 'block';
}
function displayScoreBreakdown(breakdown) {
const el = document.getElementById('scoreBreakdown');
if (!el || !breakdown) return;
el.innerHTML = `
${breakdown.checks_passed} of ${breakdown.checks_total} checks passed · Base: ${breakdown.base_score}% · Penalty: −${breakdown.penalty} · Score: ${breakdown.final_score}
Check Result
${breakdown.per_check.map(c => {
const rowId = 'check-row-' + c.name.replace(/\s+/g, '-');
const isOverridden = overriddenChecks.has(c.name);
let resultCell;
if (c.passed) {
resultCell = `✓ Pass `;
} else if (isOverridden) {
resultCell = `✓ Manual Pass
↩ Undo `;
} else {
resultCell = `✗ Fail
✓ Mark as Passed `;
}
return `${c.name} ${resultCell} `;
}).join('')}
`;
}
// Maps H-type Matterhorn checkpoint IDs to the Score Breakdown check names that drive them
const CP_TO_CHECK = { '04': 'Color Contrast', '13': 'Image Accessibility', '14': 'Heading Structure' };
function displayMatterhorn(summary) {
const card = document.getElementById('matterhornCard');
const banner = document.getElementById('matterhornBanner');
const body = document.getElementById('matterhornBody');
if (!card || !summary) return;
card.style.display = 'block';
const cpMap = {};
summary.checkpoints.forEach(cp => { cpMap[cp.id] = cp; });
// Compute effective status: FAIL → MANUAL_PASS if linked check is overridden
function effectiveStatus(cp) {
if (cp.status === 'FAIL') {
const linked = CP_TO_CHECK[cp.id];
if (linked && overriddenChecks.has(linked)) return 'MANUAL_PASS';
}
return cp.status;
}
// Recompute overall_passed based on effective statuses
const effectivelyAllPassed = summary.checkpoints.every(cp => {
const s = effectiveStatus(cp);
return s === 'PASS' || s === 'MANUAL_PASS' || s === 'NOT_TESTED';
});
banner.innerHTML = effectivelyAllPassed
? `✅ PDF/UA-1 requirements fulfilled
`
: `❌ PDF/UA-1 requirements NOT fulfilled
`;
const sections = [
{ label: 'Basic Requirements', ids: ['01','02','03','04','05','06','07','08'] },
{ label: 'Logical Structure', ids: ['09','10','11','12','13','14','15','16','17','18','19','20'] },
{ label: 'Document Elements', ids: ['21','22','23','24','25','26','27','28','29','30','31'] },
];
let html = '';
sections.forEach(section => {
html += ``;
section.ids.forEach(id => {
const cp = cpMap[id];
if (!cp) return;
const effStatus = effectiveStatus(cp);
const howBadge = cp.how === 'M'
? `M `
: `H `;
let statusHtml;
if (effStatus === 'MANUAL_PASS') {
const linked = CP_TO_CHECK[cp.id];
statusHtml = `✓ Manual Pass
↩ Undo `;
} else if (effStatus === 'PASS') {
statusHtml = `✓ PASS `;
} else if (effStatus === 'FAIL' && CP_TO_CHECK[cp.id]) {
const linked = CP_TO_CHECK[cp.id];
statusHtml = `✗ FAIL
✓ Mark as Passed `;
} else if (effStatus === 'FAIL') {
statusHtml = `✗ FAIL `;
} else {
statusHtml = `— Not tested `;
}
html += `
CP${cp.id} ${cp.name}
${howBadge}
${statusHtml}
`;
});
});
body.innerHTML = html;
}
async function dismissIssue(globalIndex) {
try {
const resp = await fetch('api.php?action=dismiss', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ job_id: currentJobId, issue_index: globalIndex })
});
const result = await resp.json();
if (result.success) {
dismissedIndices.add(globalIndex);
const el = document.getElementById('issue-g' + globalIndex);
if (el) {
el.classList.add('dismissed');
el.querySelector('.issue-description').style.textDecoration = 'line-through';
const btn = el.querySelector('.btn-dismiss');
if (btn) { btn.className = 'btn-undismiss'; btn.textContent = 'Restore'; btn.setAttribute('onclick', `undismissIssue(${globalIndex})`); }
}
updateDismissCount();
}
} catch(e) { console.error('Dismiss failed:', e); }
}
async function undismissIssue(globalIndex) {
try {
const resp = await fetch('api.php?action=undismiss', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ job_id: currentJobId, issue_index: globalIndex })
});
const result = await resp.json();
if (result.success) {
dismissedIndices.delete(globalIndex);
const el = document.getElementById('issue-g' + globalIndex);
if (el) {
el.classList.remove('dismissed');
el.querySelector('.issue-description').style.textDecoration = '';
const btn = el.querySelector('.btn-undismiss');
if (btn) { btn.className = 'btn-dismiss'; btn.textContent = 'Dismiss'; btn.setAttribute('onclick', `dismissIssue(${globalIndex})`); }
}
updateDismissCount();
}
} catch(e) { console.error('Undismiss failed:', e); }
}
function updateDismissCount() {
const countEl = document.getElementById('dismissedCount');
if (countEl) countEl.textContent = dismissedIndices.size;
}
async function overrideCheck(checkName) {
try {
const resp = await fetch('api.php?action=override_check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ job_id: currentJobId, check_name: checkName })
});
const result = await resp.json();
if (result.success) {
overriddenChecks.add(checkName);
// DOM-patch: swap row to Manual Pass + Undo button
const rowId = 'check-row-' + checkName.replace(/\s+/g, '-');
const row = document.getElementById(rowId);
if (row) {
const td = row.querySelector('td:last-child');
if (td) td.innerHTML = `✓ Manual Pass
↩ Undo `;
}
renderRecalcButton();
// Refresh Matterhorn table so CP status reflects the override
if (lastMatterhornData) displayMatterhorn(lastMatterhornData);
}
} catch(e) { console.error('Override failed:', e); }
}
async function unoverrideCheck(checkName) {
try {
const resp = await fetch('api.php?action=unoverride_check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ job_id: currentJobId, check_name: checkName })
});
const result = await resp.json();
if (result.success) {
overriddenChecks.delete(checkName);
// DOM-patch: revert row to Fail + Mark as Passed button
const rowId = 'check-row-' + checkName.replace(/\s+/g, '-');
const row = document.getElementById(rowId);
if (row) {
const td = row.querySelector('td:last-child');
if (td) td.innerHTML = `✗ Fail
✓ Mark as Passed `;
}
renderRecalcButton();
// Refresh Matterhorn table so CP status reflects the removal
if (lastMatterhornData) displayMatterhorn(lastMatterhornData);
}
} catch(e) { console.error('Unoverride failed:', e); }
}
function renderRecalcButton() {
const btn = document.getElementById('recheckBtn');
if (btn) btn.style.display = 'inline-block';
}
// Pure DOM update — called both on user action and on initial load of adjusted result
function applyScoreRecalc() {
if (!scoreBreakdownData || !originalSeverityCounts) return null;
const bd = scoreBreakdownData;
const origSC = originalSeverityCounts;
// 1. Adjust severity counts for dismissed issues
let adj_crit = origSC.critical || 0;
let adj_err = origSC.error || 0;
dismissedIndices.forEach(idx => {
const sev = (allIssues[idx]?.severity || '').toUpperCase();
if (sev === 'CRITICAL') adj_crit = Math.max(0, adj_crit - 1);
if (sev === 'ERROR') adj_err = Math.max(0, adj_err - 1);
});
// 2. New penalty
const new_penalty = Math.min(20, adj_crit * 5 + adj_err * 2);
// 3. New base from overridden checks
const new_passed = Math.min(bd.checks_total, bd.checks_passed + overriddenChecks.size);
const new_base = bd.checks_total > 0 ? Math.round(100 * new_passed / bd.checks_total) : 0;
// 4. Final score
const new_score = Math.max(0, new_base - new_penalty);
// 5. Update DOM
document.getElementById('scoreNumber').textContent = new_score;
const lbl = document.getElementById('adjustedLabel');
if (lbl) lbl.style.display = 'inline';
updateStatsGrid(adj_crit, adj_err);
updateBreakdownSummary(new_passed, bd.checks_total, new_base, new_penalty, new_score);
// 6. Recompute WCAG compliance badges
const failingA = [], failingAA = [];
allIssues.forEach((issue, idx) => {
if (dismissedIndices.has(idx)) return;
const sev = (issue.severity || '').toUpperCase();
if (sev !== 'CRITICAL' && sev !== 'ERROR') return;
const crit = issue.wcag_criterion;
if (!crit) return;
const lvl = WCAG_LEVELS[crit];
if (lvl === 'A' && !failingA.includes(crit)) failingA.push(crit);
if (lvl === 'AA' && !failingAA.includes(crit)) failingAA.push(crit);
});
displayWcagCompliance({
level_a: failingA.length === 0,
level_aa: failingA.length === 0 && failingAA.length === 0,
level_a_failures: failingA,
level_aa_failures: failingAA,
});
return new_score;
}
async function recalculateScore() {
const new_score = applyScoreRecalc();
if (new_score === null || !currentJobId) return;
// Persist adjusted result so history + exports reflect the new score
try {
const btn = document.getElementById('recheckBtn');
if (btn) { btn.disabled = true; btn.textContent = 'Saving…'; }
await fetch('api.php?action=save_adjusted_result', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ job_id: currentJobId }),
});
} catch(e) {
console.error('Save adjusted failed:', e);
} finally {
const btn = document.getElementById('recheckBtn');
if (btn) { btn.disabled = false; btn.textContent = 'Recalculate Score'; }
}
}
function updateStatsGrid(adj_crit, adj_err) {
const critCard = document.querySelector('.stat-card.critical .stat-number');
const errCard = document.querySelector('.stat-card.error .stat-number');
if (critCard) critCard.textContent = adj_crit;
if (errCard) errCard.textContent = adj_err;
}
function updateBreakdownSummary(new_passed, checks_total, new_base, new_penalty, new_score) {
const summary = document.getElementById('scoreBreakdownSummary');
if (summary) {
summary.innerHTML = `${new_passed} of ${checks_total} checks passed · Base: ${new_base}% · Penalty: −${new_penalty} · Score: ${new_score} (Adjusted) `;
}
}