pdf-accessibility/js/results.js
Vadym Samoilenko e60639c58d Add SSO user isolation and document history dashboard
- api.php: extractUserFromToken() decodes Azure AD JWT payload (oid/name/email)
- Upload: stores user_id, user_name, user_email in job .meta.json
- handleList(): filters jobs by authenticated user's oid — full user isolation
  (jobs without user_id are excluded for authenticated users to prevent leakage);
  enriches each entry with score, grade, critical/error counts from result JSON
- index.html: "My Documents" history section, shown after login
- js/app.js: showAuthenticatedUI() triggers loadHistory(); full renderHistory()
  renders sortable table with score, grade, severity badges, and Open/HTML/PDF/JSON
  action buttons; openHistoryJob() loads any past result into the results panel
- js/results.js: calls loadHistory() after displayResults() so table refreshes
  immediately after a new check completes
- css/styles.css: history table styles with colour-coded score/grade/severity badges

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 14:58:18 +00:00

545 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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;
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 = `
<div class="stat-card critical"><div class="stat-number">${sc.critical}</div><div class="stat-label">Critical</div></div>
<div class="stat-card error"><div class="stat-number">${sc.error}</div><div class="stat-label">Errors</div></div>
<div class="stat-card warning"><div class="stat-number">${sc.warning}</div><div class="stat-label">Warnings</div></div>
<div class="stat-card info"><div class="stat-number">${sc.info}</div><div class="stat-label">Info</div></div>
<div class="stat-card success"><div class="stat-number">${sc.success}</div><div class="stat-label">Success</div></div>
`;
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);
initializePageViewer(data);
displayRemediationOptions(data);
displayMatterhorn(data.matterhorn_summary);
// 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 = '<p style="text-align:center;color:var(--text-light);padding:40px;">No issues to display</p>';
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 += '<div style="background:var(--surface);padding:15px;border-radius:8px;margin-bottom:20px;box-shadow:0 1px 3px rgba(0,0,0,0.1);">';
html += '<h3 style="margin-bottom:10px;font-size:16px;font-weight:600;">Page Overview</h3>';
html += '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(55px,1fr));gap:8px;">';
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 += `<div onclick="scrollToPage(${pn})" style="cursor:pointer;background:${bg};color:${warn > 0 && !crit && !err ? 'black' : 'white'};padding:10px 8px;border-radius:6px;text-align:center;font-weight:600;" aria-label="Page ${pn}, ${pi.length} issues">
<div style="font-size:10px;opacity:0.9;">Page</div>
<div style="font-size:18px;">${pn}</div>
<div style="font-size:10px;margin-top:3px;">${pi.length} issue${pi.length !== 1 ? 's' : ''}</div>
</div>`;
});
html += '</div></div>';
}
// Document-wide issues
if (documentWide.length > 0) {
html += `<div id="page-document" style="margin-bottom:30px;">
<h3 style="font-size:18px;margin-bottom:10px;padding:10px 12px;background:var(--surface-alt);border-radius:6px;cursor:pointer;" onclick="togglePageSection('document')" aria-expanded="true">
Document-Wide Issues (${documentWide.length}) <span id="toggle-document" style="float:right;">&#9660;</span>
</h3>
<div id="section-document" class="issues-grid">${documentWide.map(i => createIssueCard(i, issueNumberMap.get(i), allIssues.indexOf(i))).join('')}</div>
</div>`;
}
// 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 += `<div id="page-${pn}" style="margin-bottom:20px;">
<h3 style="font-size:18px;margin-bottom:10px;padding:10px 12px;background:var(--surface-alt);border-radius:6px;cursor:pointer;" onclick="togglePageSection(${pn})" aria-expanded="true">
Page ${pn} - ${pi.length} Issue${pi.length !== 1 ? 's' : ''}
${crit > 0 ? `<span style="background:#dc2626;color:white;padding:2px 6px;border-radius:10px;font-size:11px;margin-left:8px;">${crit} Critical</span>` : ''}
${err > 0 ? `<span style="background:#ef4444;color:white;padding:2px 6px;border-radius:10px;font-size:11px;margin-left:8px;">${err} Error${err !== 1 ? 's' : ''}</span>` : ''}
${warn > 0 ? `<span style="background:#f59e0b;color:white;padding:2px 6px;border-radius:10px;font-size:11px;margin-left:8px;">${warn} Warning${warn !== 1 ? 's' : ''}</span>` : ''}
<span id="toggle-${pn}" style="float:right;">&#9660;</span>
</h3>
<div id="section-${pn}" class="issues-grid">${pi.map(i => createIssueCard(i, issueNumberMap.get(i), allIssues.indexOf(i))).join('')}</div>
</div>`;
});
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
? `<button onclick="viewOnPage(${issue.page_number}, ${issueNumber})" class="btn-dismiss" style="background:var(--accent);color:var(--accent-text);border:none;" title="View on page">&#x1F4CD; #${issueNumber}</button>`
: '';
const dismissBtn = isDismissed
? `<button class="btn-undismiss" onclick="undismissIssue(${globalIndex})" title="Restore this issue">&#x21A9; Restore</button>`
: `<button class="btn-dismiss" onclick="dismissIssue(${globalIndex})" title="Mark as false positive / reviewed">&#x2715; False Positive</button>`;
return `<div class="issue ${issue.severity}${isDismissed ? ' dismissed' : ''}" id="issue-g${globalIndex}" role="listitem">
<div class="issue-header">
<div class="issue-category"><span style="font-size:16px;">${catIcon}</span><span>${issue.category}</span>${markerBadge}</div>
<div style="display:flex;align-items:center;gap:4px;">
<span class="issue-badge ${issue.severity}"><span>${icon}</span><span>${issue.severity}</span></span>
${dismissBtn}
</div>
</div>
<div class="issue-description">${issue.description}</div>
${issue.wcag_criterion ? `<div class="issue-meta">
${wcagCriterionLinks(issue.wcag_criterion)}
${issue.wcag_level ? `<span class="wcag-level-badge wcag-level-${issue.wcag_level}" aria-label="WCAG Level ${issue.wcag_level}">${issue.wcag_level}</span>` : ''}
</div>` : ''}
${issue.recommendation ? `<div class="issue-recommendation"><strong>Tip:</strong> ${issue.recommendation}</div>` : ''}
</div>`;
}
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 = '&#9660;';
if (header) header.setAttribute('aria-expanded', 'true');
} else {
section.style.display = 'none';
toggle.innerHTML = '&#9654;';
if (header) header.setAttribute('aria-expanded', 'false');
}
}
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 = '<div style="background:var(--success-bg);padding:12px;border-radius:6px;border-left:3px solid var(--success);">';
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 += `<div style="margin-bottom:8px;display:flex;align-items:start;gap:8px;">
<span style="font-size:16px;">${ic[fix.severity] || '\u{1F527}'}</span>
<div style="flex:1;"><div style="font-weight:600;font-size:13px;">${fix.description}</div>
<div style="font-size:12px;color:var(--text-light);margin-top:2px;">Will set: ${fix.suggestion}</div></div>
</div>`;
});
}
html += '</div>';
fixesList.innerHTML = html;
}
async function applyFixes() {
const btn = document.getElementById('applyFixesBtn');
const resultDiv = document.getElementById('fixResult');
btn.disabled = true;
btn.innerHTML = '<span class="loading"></span> Applying fixes...';
resultDiv.style.display = 'block';
resultDiv.innerHTML = '<div style="padding:10px;background:var(--info-bg);border-radius:6px;color:var(--text);">Applying automatic fixes to PDF...</div>';
try {
const result = await remediatePdf(currentJobId);
if (result.success) {
resultDiv.innerHTML = `<div style="padding:15px;background:var(--success-bg);border-radius:6px;border-left:3px solid var(--success);">
<div style="font-weight:600;margin-bottom:8px;color:var(--success);">${result.data.fixes_applied} issue(s) automatically fixed!</div>
<div style="font-size:14px;margin-bottom:12px;color:var(--text);">Your remediated PDF is ready for download.</div>
<a href="${result.data.download_url}" class="btn btn-primary" download style="text-decoration:none;display:inline-block;">Download Fixed PDF</a>
<div style="margin-top:10px;font-size:12px;color:var(--text-light);">Filename: ${result.data.original_filename.replace('.pdf', '_fixed.pdf')}</div>
</div>`;
btn.style.display = 'none';
} else {
resultDiv.innerHTML = `<div style="padding:15px;background:var(--error-bg);border-radius:6px;border-left:3px solid var(--error);">
<div style="font-weight:600;color:var(--error);">Remediation failed</div>
<div style="font-size:13px;margin-top:5px;">${result.error}</div>
</div>`;
btn.disabled = false;
btn.innerHTML = '<span>Retry Auto-Fix</span>';
}
} catch (error) {
resultDiv.innerHTML = `<div style="padding:15px;background:var(--error-bg);border-radius:6px;border-left:3px solid var(--error);">
<div style="font-weight:600;color:var(--error);">Error</div>
<div style="font-size:13px;margin-top:5px;">${error.message}</div>
</div>`;
btn.disabled = false;
btn.innerHTML = '<span>Retry Auto-Fix</span>';
}
}
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 = `
<div class="wcag-compliance-row">
<div class="wcag-badge ${levelA ? 'pass' : 'fail'}" aria-label="WCAG 2.1 Level A ${levelA ? 'pass' : 'fail'}">
<span class="wcag-badge-level">WCAG 2.1 A</span>
<span class="wcag-badge-status">${levelA ? '✓ Pass' : '✗ Fail'}</span>
</div>
<div class="wcag-badge ${levelAA ? 'pass' : 'fail'}" aria-label="WCAG 2.1 Level AA ${levelAA ? 'pass' : 'fail'}">
<span class="wcag-badge-level">WCAG 2.1 AA</span>
<span class="wcag-badge-status">${levelAA ? '✓ Pass' : '✗ Fail'}</span>
</div>
</div>
${!levelA && aFailures ? `<p class="compliance-failures" aria-label="Level A failures">Level A failing criteria: <strong>${aFailures}</strong></p>` : ''}
${!levelAA && !levelA && aaFailures ? `<p class="compliance-failures" aria-label="Level AA failures">Level AA failing criteria: <strong>${aaFailures}</strong></p>` : ''}
`;
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) => `
<li class="next-step-item">
<span class="next-step-num" aria-hidden="true">${i + 1}</span>
<div class="next-step-body">
<div class="next-step-action">${s.action}</div>
<div class="next-step-meta">
<span class="issue-badge ${priorityClass[s.priority] || 'INFO'}" style="font-size:11px;padding:2px 6px;">${priorityLabel[s.priority] || ''}</span>
<span style="font-size:12px;color:var(--text-muted);">${s.category}</span>
${s.wcag ? `<span style="font-size:12px;color:var(--text-muted);">${wcagCriterionLinks(s.wcag)}</span>` : ''}
${s.wcag_level ? `<span class="wcag-level-badge wcag-level-${s.wcag_level}">${s.wcag_level}</span>` : ''}
</div>
</div>
</li>
`).join('');
el.style.display = 'block';
}
function displayScoreBreakdown(breakdown) {
const el = document.getElementById('scoreBreakdown');
if (!el || !breakdown) return;
el.innerHTML = `
<details class="score-breakdown" id="scoreBreakdownDetails">
<summary id="scoreBreakdownSummary">${breakdown.checks_passed} of ${breakdown.checks_total} checks passed &nbsp;·&nbsp; Base: ${breakdown.base_score}% &nbsp;·&nbsp; Penalty: ${breakdown.penalty} &nbsp;·&nbsp; Score: ${breakdown.final_score}</summary>
<table class="score-breakdown-table">
<thead><tr><th>Check</th><th>Result</th></tr></thead>
<tbody>
${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 = `<span style="color:var(--success);font-weight:700;">✓ Pass</span>`;
} else if (isOverridden) {
resultCell = `<span class="check-manual-pass"> Manual Pass</span>
<button class="btn-unoverride" onclick="unoverrideCheck('${escapeAttr(c.name)}')"> Undo</button>`;
} else {
resultCell = `<span style="color:var(--error);font-weight:700;">✗ Fail</span>
<button class="btn-mark-passed" onclick="overrideCheck('${escapeAttr(c.name)}')">✓ Mark as Passed</button>`;
}
return `<tr id="${rowId}"><td>${c.name}</td><td>${resultCell}</td></tr>`;
}).join('')}
</tbody>
</table>
</details>`;
}
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';
banner.innerHTML = summary.overall_passed
? `<div class="matterhorn-banner pass">&#x2705; PDF/UA-1 requirements fulfilled</div>`
: `<div class="matterhorn-banner fail">&#x274C; PDF/UA-1 requirements NOT fulfilled</div>`;
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'] },
];
const cpMap = {};
summary.checkpoints.forEach(cp => { cpMap[cp.id] = cp; });
let html = '';
sections.forEach(section => {
html += `<tr class="section-header"><td colspan="3">${section.label}</td></tr>`;
section.ids.forEach(id => {
const cp = cpMap[id];
if (!cp) return;
const statusHtml = cp.status === 'PASS'
? `<span class="mh-pass">&#x2713; PASS</span>`
: cp.status === 'FAIL'
? `<span class="mh-fail">&#x2717; FAIL</span>`
: `<span class="mh-not-tested">— Not tested</span>`;
const howBadge = cp.how === 'M'
? `<span class="badge-m">M</span>`
: `<span class="badge-h">H</span>`;
html += `<tr>
<td><strong>CP${cp.id}</strong> ${cp.name}</td>
<td>${howBadge}</td>
<td>${statusHtml}</td>
</tr>`;
});
});
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 = `<span class="check-manual-pass">✓ Manual Pass</span>
<button class="btn-unoverride" onclick="unoverrideCheck('${escapeAttr(checkName)}')">↩ Undo</button>`;
}
renderRecalcButton();
}
} 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 = `<span style="color:var(--error);font-weight:700;">✗ Fail</span>
<button class="btn-mark-passed" onclick="overrideCheck('${escapeAttr(checkName)}')">✓ Mark as Passed</button>`;
}
renderRecalcButton();
}
} catch(e) { console.error('Unoverride failed:', e); }
}
function renderRecalcButton() {
const btn = document.getElementById('recheckBtn');
if (btn) btn.style.display = 'inline-block';
}
function recalculateScore() {
if (!scoreBreakdownData || !originalSeverityCounts) return;
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 + grade
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);
}
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 &nbsp;·&nbsp; Base: ${new_base}% &nbsp;·&nbsp; Penalty: ${new_penalty} &nbsp;·&nbsp; Score: ${new_score} <em style="font-size:11px;opacity:0.7;">(Adjusted)</em>`;
}
}