PDF report reflects adjusted score + manual pass for Matterhorn H-type CPs
- api.php: add save_adjusted_result action that merges dismissed issues,
check overrides and recalculated score into {job_id}.adjusted.json;
handleExport() now prefers .adjusted.json over .result.json
- js/results.js: displayMatterhorn() shows Mark as Passed / Undo buttons
for H-type CPs (CP04, CP13) linked to overridden checks; overrideCheck /
unoverrideCheck refresh Matterhorn table and recompute overall banner
- js/batch.js: exportReport() saves adjusted result before opening export
URL, using pre-opened window to avoid popup blockers
- report_generator.py: filter dismissed issues, show (Adjusted) badge,
Manual Pass in checks and Matterhorn tables; switch generate_html() to
Montserrat + Oliver branding (#1a1a1a header, #FFC407 skip-link)
- css/styles.css: fix dark-mode log-header from blue-ish #252840 to #242424
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5858f3c90d
commit
79aaf050bf
5 changed files with 223 additions and 26 deletions
121
api.php
121
api.php
|
|
@ -354,6 +354,9 @@ switch ($action) {
|
|||
case 'export':
|
||||
handleExport();
|
||||
break;
|
||||
case 'save_adjusted_result':
|
||||
handleSaveAdjustedResult();
|
||||
break;
|
||||
case 'dismiss':
|
||||
handleDismiss();
|
||||
break;
|
||||
|
|
@ -1167,7 +1170,10 @@ function handleExport() {
|
|||
}
|
||||
$job_id = sanitizeJobId($job_id);
|
||||
|
||||
$result_file = RESULTS_DIR . '/' . $job_id . '.result.json';
|
||||
// Prefer adjusted result if available (created by save_adjusted_result)
|
||||
$adj_file = RESULTS_DIR . '/' . $job_id . '.adjusted.json';
|
||||
$result_file = file_exists($adj_file) ? $adj_file : RESULTS_DIR . '/' . $job_id . '.result.json';
|
||||
|
||||
if (!file_exists($result_file)) {
|
||||
error('Results not found');
|
||||
}
|
||||
|
|
@ -1231,6 +1237,119 @@ function handleExport() {
|
|||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an adjusted result merging dismissed issues and check overrides into a new JSON file.
|
||||
* The export endpoint will prefer this file over the original result.
|
||||
*/
|
||||
function handleSaveAdjustedResult() {
|
||||
$data = json_decode(file_get_contents('php://input'), true) ?: [];
|
||||
$job_id = $data['job_id'] ?? '';
|
||||
|
||||
if (empty($job_id)) {
|
||||
error('job_id required');
|
||||
}
|
||||
$job_id = sanitizeJobId($job_id);
|
||||
|
||||
$result_file = RESULTS_DIR . '/' . $job_id . '.result.json';
|
||||
if (!file_exists($result_file)) {
|
||||
error('Results not found');
|
||||
}
|
||||
|
||||
$result = json_decode(file_get_contents($result_file), true);
|
||||
|
||||
// Load dismissed and overrides
|
||||
$dismiss_file = RESULTS_DIR . '/' . $job_id . '.dismissed.json';
|
||||
$override_file = RESULTS_DIR . '/' . $job_id . '.overrides.json';
|
||||
$dismissed = file_exists($dismiss_file) ? json_decode(file_get_contents($dismiss_file), true) : [];
|
||||
$overrides = file_exists($override_file) ? json_decode(file_get_contents($override_file), true) : [];
|
||||
|
||||
// 1. Mark dismissed issues in the issues array
|
||||
if (!empty($dismissed) && isset($result['issues'])) {
|
||||
foreach ($result['issues'] as $idx => &$issue) {
|
||||
if (isset($dismissed[$idx])) {
|
||||
$issue['dismissed'] = true;
|
||||
}
|
||||
}
|
||||
unset($issue);
|
||||
}
|
||||
|
||||
// 2. Recalculate score (mirrors JS recalculateScore())
|
||||
$bd = $result['score_breakdown'] ?? [];
|
||||
$origSC = $result['severity_counts'] ?? [];
|
||||
|
||||
$adj_crit = (int)($origSC['critical'] ?? 0);
|
||||
$adj_err = (int)($origSC['error'] ?? 0);
|
||||
|
||||
// Subtract dismissed CRITICAL / ERROR issues
|
||||
foreach ($dismissed as $idx => $info) {
|
||||
$sev = strtoupper($result['issues'][$idx]['severity'] ?? '');
|
||||
if ($sev === 'CRITICAL') $adj_crit = max(0, $adj_crit - 1);
|
||||
if ($sev === 'ERROR') $adj_err = max(0, $adj_err - 1);
|
||||
}
|
||||
|
||||
$new_penalty = min(20, $adj_crit * 5 + $adj_err * 2);
|
||||
$checks_total = (int)($bd['checks_total'] ?? 0);
|
||||
$checks_passed = (int)($bd['checks_passed'] ?? 0);
|
||||
$new_passed = min($checks_total, $checks_passed + count($overrides));
|
||||
$new_base = $checks_total > 0 ? (int)round(100 * $new_passed / $checks_total) : 0;
|
||||
$new_score = max(0, $new_base - $new_penalty);
|
||||
|
||||
$result['accessibility_score'] = $new_score;
|
||||
$result['severity_counts']['critical'] = $adj_crit;
|
||||
$result['severity_counts']['error'] = $adj_err;
|
||||
$result['score_breakdown']['final_score'] = $new_score;
|
||||
$result['score_breakdown']['checks_passed'] = $new_passed;
|
||||
$result['score_breakdown']['base_score'] = $new_base;
|
||||
$result['score_breakdown']['penalty'] = $new_penalty;
|
||||
$result['score_breakdown']['adjusted'] = true;
|
||||
|
||||
// 3. Mark overridden checks in checks_performed
|
||||
if (!empty($overrides) && isset($result['checks_performed'])) {
|
||||
foreach ($result['checks_performed'] as &$check) {
|
||||
if (isset($overrides[$check['name']])) {
|
||||
$check['passed'] = true;
|
||||
$check['manual'] = true;
|
||||
}
|
||||
}
|
||||
unset($check);
|
||||
}
|
||||
|
||||
// 4. Update Matterhorn checkpoints for H-type CPs linked to overridden checks
|
||||
$check_to_cp = [
|
||||
'Color Contrast' => ['04'],
|
||||
'Image Accessibility' => ['13'],
|
||||
];
|
||||
$cp_to_check = [];
|
||||
foreach ($check_to_cp as $checkName => $cpIds) {
|
||||
foreach ($cpIds as $cpId) {
|
||||
$cp_to_check[$cpId] = $checkName;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($overrides) && isset($result['matterhorn_summary']['checkpoints'])) {
|
||||
foreach ($result['matterhorn_summary']['checkpoints'] as &$cp) {
|
||||
$cpId = $cp['id'];
|
||||
if (isset($cp_to_check[$cpId]) && isset($overrides[$cp_to_check[$cpId]])) {
|
||||
$cp['status'] = 'PASS';
|
||||
$cp['manual'] = true;
|
||||
}
|
||||
}
|
||||
unset($cp);
|
||||
|
||||
// Recompute overall_passed
|
||||
$all_pass = true;
|
||||
foreach ($result['matterhorn_summary']['checkpoints'] as $cp) {
|
||||
if ($cp['status'] === 'FAIL') { $all_pass = false; break; }
|
||||
}
|
||||
$result['matterhorn_summary']['overall_passed'] = $all_pass;
|
||||
}
|
||||
|
||||
$adj_file = RESULTS_DIR . '/' . $job_id . '.adjusted.json';
|
||||
file_put_contents($adj_file, json_encode($result));
|
||||
|
||||
success(['saved' => true, 'score' => $new_score]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss an issue (mark as false positive)
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -508,7 +508,7 @@ h1::before {
|
|||
}
|
||||
|
||||
:root[data-theme="dark"] .log-header {
|
||||
background: #252840;
|
||||
background: #242424;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
|
|
|
|||
28
js/batch.js
28
js/batch.js
|
|
@ -273,8 +273,32 @@ async function viewBatchResult(jobId) {
|
|||
}
|
||||
}
|
||||
|
||||
function exportReport(format) {
|
||||
async function exportReport(format) {
|
||||
if (!currentJobId) return;
|
||||
|
||||
const hasAdjustments =
|
||||
(typeof overriddenChecks !== 'undefined' && overriddenChecks.size > 0) ||
|
||||
(typeof dismissedIndices !== 'undefined' && dismissedIndices.size > 0);
|
||||
|
||||
// Open the window synchronously first to avoid popup-blocker blocking an async call
|
||||
const win = window.open('about:blank', '_blank');
|
||||
|
||||
if (hasAdjustments) {
|
||||
try {
|
||||
await fetch('api.php?action=save_adjusted_result', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ job_id: currentJobId })
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('Could not save adjusted result before export:', e);
|
||||
}
|
||||
}
|
||||
|
||||
const url = getExportUrl(currentJobId, format);
|
||||
window.open(url, '_blank');
|
||||
if (win) {
|
||||
win.location.href = url;
|
||||
} else {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ let dismissedIndices = new Set();
|
|||
let overriddenChecks = new Set();
|
||||
let scoreBreakdownData = null;
|
||||
let originalSeverityCounts = null;
|
||||
let lastMatterhornData = null;
|
||||
|
||||
function displayResults(data) {
|
||||
document.getElementById('uploadSection').style.display = 'none';
|
||||
|
|
@ -38,7 +39,8 @@ function displayResults(data) {
|
|||
displayIssues(allIssues);
|
||||
initializePageViewer(data);
|
||||
displayRemediationOptions(data);
|
||||
displayMatterhorn(data.matterhorn_summary);
|
||||
lastMatterhornData = data.matterhorn_summary || null;
|
||||
displayMatterhorn(lastMatterhornData);
|
||||
|
||||
// Refresh history so the new result appears in the table
|
||||
if (typeof loadHistory === 'function') loadHistory();
|
||||
|
|
@ -351,6 +353,9 @@ function displayScoreBreakdown(breakdown) {
|
|||
</details>`;
|
||||
}
|
||||
|
||||
// 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' };
|
||||
|
||||
function displayMatterhorn(summary) {
|
||||
const card = document.getElementById('matterhornCard');
|
||||
const banner = document.getElementById('matterhornBanner');
|
||||
|
|
@ -359,7 +364,25 @@ function displayMatterhorn(summary) {
|
|||
|
||||
card.style.display = 'block';
|
||||
|
||||
banner.innerHTML = summary.overall_passed
|
||||
const cpMap = {};
|
||||
summary.checkpoints.forEach(cp => { cpMap[cp.id] = cp; });
|
||||
|
||||
// Compute effective status: H-type FAIL → MANUAL_PASS if linked check is overridden
|
||||
function effectiveStatus(cp) {
|
||||
if (cp.how === 'H' && 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
|
||||
? `<div class="matterhorn-banner pass">✅ PDF/UA-1 requirements fulfilled</div>`
|
||||
: `<div class="matterhorn-banner fail">❌ PDF/UA-1 requirements NOT fulfilled</div>`;
|
||||
|
||||
|
|
@ -369,23 +392,35 @@ function displayMatterhorn(summary) {
|
|||
{ 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">✓ PASS</span>`
|
||||
: cp.status === 'FAIL'
|
||||
? `<span class="mh-fail">✗ FAIL</span>`
|
||||
: `<span class="mh-not-tested">— Not tested</span>`;
|
||||
|
||||
const effStatus = effectiveStatus(cp);
|
||||
const howBadge = cp.how === 'M'
|
||||
? `<span class="badge-m">M</span>`
|
||||
: `<span class="badge-h">H</span>`;
|
||||
|
||||
let statusHtml;
|
||||
if (effStatus === 'MANUAL_PASS') {
|
||||
const linked = CP_TO_CHECK[cp.id];
|
||||
statusHtml = `<span class="check-manual-pass">✓ Manual Pass</span>
|
||||
<button class="btn-unoverride" onclick="unoverrideCheck('${escapeAttr(linked)}')">↩ Undo</button>`;
|
||||
} else if (effStatus === 'PASS') {
|
||||
statusHtml = `<span class="mh-pass">✓ PASS</span>`;
|
||||
} else if (effStatus === 'FAIL' && cp.how === 'H' && CP_TO_CHECK[cp.id]) {
|
||||
const linked = CP_TO_CHECK[cp.id];
|
||||
statusHtml = `<span class="mh-fail">✗ FAIL</span>
|
||||
<button class="btn-mark-passed" onclick="overrideCheck('${escapeAttr(linked)}')">✓ Mark as Passed</button>`;
|
||||
} else if (effStatus === 'FAIL') {
|
||||
statusHtml = `<span class="mh-fail">✗ FAIL</span>`;
|
||||
} else {
|
||||
statusHtml = `<span class="mh-not-tested">— Not tested</span>`;
|
||||
}
|
||||
|
||||
html += `<tr>
|
||||
<td><strong>CP${cp.id}</strong> ${cp.name}</td>
|
||||
<td>${howBadge}</td>
|
||||
|
|
@ -464,6 +499,8 @@ async function overrideCheck(checkName) {
|
|||
<button class="btn-unoverride" onclick="unoverrideCheck('${escapeAttr(checkName)}')">↩ Undo</button>`;
|
||||
}
|
||||
renderRecalcButton();
|
||||
// Refresh Matterhorn table so CP status reflects the override
|
||||
if (lastMatterhornData) displayMatterhorn(lastMatterhornData);
|
||||
}
|
||||
} catch(e) { console.error('Override failed:', e); }
|
||||
}
|
||||
|
|
@ -487,6 +524,8 @@ async function unoverrideCheck(checkName) {
|
|||
<button class="btn-mark-passed" onclick="overrideCheck('${escapeAttr(checkName)}')">✓ Mark as Passed</button>`;
|
||||
}
|
||||
renderRecalcButton();
|
||||
// Refresh Matterhorn table so CP status reflects the removal
|
||||
if (lastMatterhornData) displayMatterhorn(lastMatterhornData);
|
||||
}
|
||||
} catch(e) { console.error('Unoverride failed:', e); }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,12 +52,13 @@ def generate_html(data: dict) -> str:
|
|||
score = data.get("accessibility_score", 0)
|
||||
grade = grade_from_score(score)
|
||||
sc = data.get("severity_counts", {})
|
||||
issues = data.get("issues", [])
|
||||
issues = [i for i in data.get("issues", []) if not i.get("dismissed")]
|
||||
checks = data.get("checks_performed", [])
|
||||
filename = data.get("filename", "Unknown")
|
||||
total_pages = data.get("total_pages", 0)
|
||||
stats = data.get("stats", {})
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
is_adjusted = data.get("score_breakdown", {}).get("adjusted", False)
|
||||
|
||||
# Score ring color
|
||||
if score >= 80:
|
||||
|
|
@ -93,8 +94,15 @@ def generate_html(data: dict) -> str:
|
|||
# Build checks table
|
||||
check_rows = []
|
||||
for ch in checks:
|
||||
status = "PASS" if ch.get("passed") else "FAIL"
|
||||
status_color = "#10b981" if ch.get("passed") else "#ef4444"
|
||||
if ch.get("manual"):
|
||||
status = "Manual Pass"
|
||||
status_color = "#d97706"
|
||||
elif ch.get("passed"):
|
||||
status = "PASS"
|
||||
status_color = "#10b981"
|
||||
else:
|
||||
status = "FAIL"
|
||||
status_color = "#ef4444"
|
||||
dur = f"{ch.get('duration', 0):.2f}s"
|
||||
check_rows.append(f"""
|
||||
<tr>
|
||||
|
|
@ -182,13 +190,15 @@ def generate_html(data: dict) -> str:
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="WCAG 2.1 accessibility report for {filename}">
|
||||
<title>Accessibility Report — {filename}</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
* {{ margin:0; padding:0; box-sizing:border-box; }}
|
||||
body {{ font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; background:#f8fafc; color:#1e293b; line-height:1.6; }}
|
||||
.skip-link {{ position:absolute; top:-100%; left:16px; background:#2563eb; color:#fff; font-size:14px; font-weight:700; padding:10px 20px; border-radius:4px; text-decoration:none; z-index:9999; }}
|
||||
body {{ font-family:'Montserrat',sans-serif; background:#f8fafc; color:#1e293b; line-height:1.6; }}
|
||||
.skip-link {{ position:absolute; top:-100%; left:16px; background:#FFC407; color:#000; font-size:14px; font-weight:700; padding:10px 20px; border-radius:4px; text-decoration:none; z-index:9999; }}
|
||||
.skip-link:focus {{ top:10px; }}
|
||||
.container {{ max-width:1100px; margin:0 auto; padding:20px; }}
|
||||
header {{ background:linear-gradient(135deg,#1e3a5f,#2563eb); color:#fff; padding:30px 0; }}
|
||||
header {{ background:#1a1a1a; color:#fff; padding:30px 0; border-left:4px solid #FFC407; }}
|
||||
header h1 {{ font-size:24px; margin-bottom:5px; }}
|
||||
header p {{ opacity:0.85; font-size:14px; }}
|
||||
.card {{ background:#fff; border-radius:12px; box-shadow:0 1px 3px rgba(0,0,0,0.1); padding:25px; margin-bottom:20px; }}
|
||||
|
|
@ -224,7 +234,7 @@ def generate_html(data: dict) -> str:
|
|||
footer {{ text-align:center; padding:20px; color:#64748b; font-size:12px; border-top:1px solid #e2e8f0; margin-top:10px; }}
|
||||
a {{ color:#2563eb; }}
|
||||
a:focus {{ outline:3px solid #2563eb; outline-offset:2px; border-radius:2px; }}
|
||||
@media print {{ body {{ background:#fff; }} .card {{ box-shadow:none; border:1px solid #e2e8f0; }} header {{ background:#1e3a5f !important; -webkit-print-color-adjust:exact; print-color-adjust:exact; }} }}
|
||||
@media print {{ body {{ background:#fff; }} .card {{ box-shadow:none; border:1px solid #e2e8f0; }} header {{ background:#1a1a1a !important; -webkit-print-color-adjust:exact; print-color-adjust:exact; }} }}
|
||||
@media (max-width:600px) {{ .score-section {{ flex-direction:column; align-items:stretch; }} .score-ring {{ margin:0 auto; }} }}
|
||||
@media (prefers-reduced-motion:reduce) {{ * {{ transition:none !important; animation:none !important; }} }}
|
||||
</style>
|
||||
|
|
@ -247,9 +257,10 @@ def generate_html(data: dict) -> str:
|
|||
<section class="card" aria-labelledby="score-heading">
|
||||
<h2 id="score-heading">Accessibility Score</h2>
|
||||
<div class="score-section">
|
||||
<div class="score-ring" role="img" aria-label="Score: {score} out of 100, Grade {grade}">
|
||||
<div class="score-ring" role="img" aria-label="Score: {score} out of 100, Grade {grade}{' (Adjusted)' if is_adjusted else ''}">
|
||||
<div class="score-number" aria-hidden="true">{score}</div>
|
||||
<div class="score-grade" aria-hidden="true">Grade {grade}</div>
|
||||
{'<div style="font-size:10px;color:#d97706;font-weight:600;margin-top:2px;">(Adjusted)</div>' if is_adjusted else ''}
|
||||
</div>
|
||||
<div class="stats-grid" role="group" aria-label="Issue counts by severity">
|
||||
<div class="stat critical"><div class="stat-num">{sc.get('critical',0)}</div><div class="stat-label">Critical</div></div>
|
||||
|
|
@ -330,7 +341,7 @@ def generate_pdf(data: dict) -> bytes:
|
|||
score = data.get("accessibility_score", 0)
|
||||
grade = grade_from_score(score)
|
||||
sc = data.get("severity_counts", {})
|
||||
issues = data.get("issues", [])
|
||||
issues = [i for i in data.get("issues", []) if not i.get("dismissed")]
|
||||
checks = data.get("checks_performed", [])
|
||||
filename = data.get("filename", "Unknown")
|
||||
total_pages = data.get("total_pages", 0)
|
||||
|
|
@ -338,6 +349,7 @@ def generate_pdf(data: dict) -> bytes:
|
|||
|
||||
matterhorn = data.get("matterhorn_summary", {})
|
||||
breakdown = data.get("score_breakdown", {})
|
||||
is_adjusted = breakdown.get("adjusted", False)
|
||||
|
||||
score_color = "#059669" if score >= 80 else "#d97706" if score >= 60 else "#dc2626"
|
||||
|
||||
|
|
@ -348,7 +360,9 @@ def generate_pdf(data: dict) -> bytes:
|
|||
mh_rows = ""
|
||||
for cp in matterhorn["checkpoints"]:
|
||||
status = cp["status"]
|
||||
if status == "PASS":
|
||||
if status == "PASS" and cp.get("manual"):
|
||||
status_cell = '<td class="manual-pass center">Manual Pass</td>'
|
||||
elif status == "PASS":
|
||||
status_cell = '<td class="pass center">PASS</td>'
|
||||
elif status == "FAIL":
|
||||
status_cell = '<td class="fail center">FAIL</td>'
|
||||
|
|
@ -470,6 +484,7 @@ def generate_pdf(data: dict) -> bytes:
|
|||
td {{ padding: 6px 10px; border-bottom: 1px solid #eee; vertical-align: top; }}
|
||||
tr {{ page-break-inside: avoid; }}
|
||||
.pass {{ color: #059669; font-weight: 700; }}
|
||||
.manual-pass {{ color: #d97706; font-weight: 700; }}
|
||||
.fail {{ color: #dc2626; font-weight: 700; }}
|
||||
.not-tested {{ color: #999; }}
|
||||
.critical {{ color: #dc2626; font-weight: 700; }}
|
||||
|
|
@ -495,10 +510,10 @@ def generate_pdf(data: dict) -> bytes:
|
|||
</header>
|
||||
|
||||
<main>
|
||||
<div class="score-block" role="img" aria-label="Accessibility score: {score} out of 100, Grade {grade}">
|
||||
<div class="score-block" role="img" aria-label="Accessibility score: {score} out of 100, Grade {grade}{' (Adjusted)' if is_adjusted else ''}">
|
||||
<div class="score-num" aria-hidden="true">{score}</div>
|
||||
<div class="score-info">
|
||||
<h2>Accessibility Score — Grade {grade}</h2>
|
||||
<h2>Accessibility Score — Grade {grade}{' <span style="font-size:10pt;color:#FFC407;">(Adjusted)</span>' if is_adjusted else ''}</h2>
|
||||
<p>{sc.get('critical',0)} critical {sc.get('error',0)} errors {sc.get('warning',0)} warnings {sc.get('info',0)} info</p>
|
||||
{f'<p>{breakdown.get("checks_passed",0)} of {breakdown.get("checks_total",0)} checks passed</p>' if breakdown else ''}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue