/* 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 = `
${sc.critical}
Critical
${sc.error}
Errors
${sc.warning}
Warnings
${sc.info}
Info
${sc.success}
Success
`; 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 += `
${sev} ${label} (${groupIssues.length})
${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 ? `` : ''; const dismissBtn = isDismissed ? `` : ``; return `
${catIcon}${issue.category}${markerBadge}
${icon}${issue.severity} ${dismissBtn}
${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 = `
Error
${error.message}
`; 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) => `
  • ${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} ${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`; } else { resultCell = `✗ Fail`; } return ``; }).join('')}
    CheckResult
    ${c.name}${resultCell}
    `; } // 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.label}`; 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 `; } else if (effStatus === 'PASS') { statusHtml = `✓ PASS`; } else if (effStatus === 'FAIL' && CP_TO_CHECK[cp.id]) { const linked = CP_TO_CHECK[cp.id]; statusHtml = `✗ FAIL `; } 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 `; } 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 `; } 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)`; } }