pdf-accessibility/js/upload.js
Vadym Samoilenko ac8aedf4a3 Implement QA report fixes: scoring, Matterhorn, dismiss, PDF report, UX
Part 1 — CSS/Contrast/Accessibility:
- Raise --text-muted contrast to WCAG AA (#696969 light, #9a9a9a dark)
- Add body font-size: 16px baseline
- Enlarge #themeToggle to 15px / 10px 20px padding

Part 2 — Start Button (user-controlled analysis):
- Upload no longer auto-starts check; shows ready state with filename/size
- New showReadyState() / removeFile() functions in upload.js
- beginCheck() now shows progress + hides ready state on click
- Add prominent "Check Another PDF" button at bottom of results

Part 3 — Scoring recalibration:
- Replace deduction formula with check-pass ratio + soft penalty (cap 20)
- Fix run_check() to only examine issues added by the current check
- Add score_breakdown (per-check table) to JSON output + results UI
- Downgrade readability ERROR → WARNING (advisory, not hard failure)

Part 4 — Auto-fix debugging:
- Remediation failure now returns up to 2000 chars of log (was 500)
- pdf_remediation.py: stderr output, sys.exit(0/1), output dir creation

Part 5 — Error location: View on Page button on each issue card

Part 6 — Matterhorn Protocol PDF/UA-1:
- _build_matterhorn_summary() maps 19 checks → 31 checkpoints
- Matterhorn card in index.html with grouped PASS/FAIL/Not-tested table
- Correct M/H badges per checkpoint

Part 7 — Dismiss / False Positive:
- dismissed_issues table in db/init.sql + dismiss/undismiss in db_manager.py
- api.php: dismiss/undismiss endpoints (file-backed), dismissed_indices
  injected into both handleStatus and handleResult responses
- results.js: dismissIssue/undismissIssue with visual strikethrough
- CSS: .dismissed, .btn-dismiss, .btn-undismiss styles

Part 8 — PDF Report (WeasyPrint):
- generate_pdf() in report_generator.py: PAC-style A4, Oliver branding
- api.php handleExport() supports format=pdf
- index.html: "PDF Report" download button in results header
- requirements.txt: weasyprint>=60.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 18:06:32 +00:00

219 lines
8.7 KiB
JavaScript

/* Upload handling — drag-drop, file validation, check flow */
let currentJobId = null;
let pollInterval = null;
let pollCount = 0;
function initUpload() {
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');
uploadArea.addEventListener('click', () => fileInput.click());
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
if (e.dataTransfer.files.length > 0) handleFile(e.dataTransfer.files[0]);
});
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) handleFile(e.target.files[0]);
});
}
async function handleFile(file) {
if (!file.name.toLowerCase().endsWith('.pdf')) {
alert('Please select a PDF file');
return;
}
if (file.size > 50 * 1024 * 1024) {
alert('File too large. Maximum size is 50MB.');
return;
}
clearLog();
document.getElementById('progressContainer').style.display = 'block';
updateProgress(0, 'Preparing upload...');
addLog('File selected: ' + file.name + ' (' + (file.size / 1024 / 1024).toFixed(2) + ' MB)', 'info');
try {
updateProgress(10, 'Uploading file...');
addLog('Uploading to server...', 'info');
const result = await uploadFile(file);
if (result.success) {
currentJobId = result.data.job_id;
updateProgress(20, 'Upload complete');
addLog('Upload successful — Job ID: ' + currentJobId, 'success');
document.getElementById('progressContainer').style.display = 'none';
showReadyState(file);
} else {
addLog('Upload failed: ' + result.error, 'error');
alert('Upload failed: ' + result.error);
document.getElementById('progressContainer').style.display = 'none';
}
} catch (error) {
addLog('Upload error: ' + error.message, 'error');
alert('Upload failed: ' + error.message);
document.getElementById('progressContainer').style.display = 'none';
}
}
function showReadyState(file) {
const readyDiv = document.getElementById('uploadReadyState');
if (!readyDiv) return;
document.getElementById('readyFilename').textContent = file.name;
document.getElementById('readyFilesize').textContent = (file.size / 1024 / 1024).toFixed(2) + ' MB';
readyDiv.style.display = 'block';
document.getElementById('singleUploadArea').querySelector('.upload-area').style.display = 'none';
}
function removeFile() {
currentJobId = null;
const readyDiv = document.getElementById('uploadReadyState');
if (readyDiv) readyDiv.style.display = 'none';
document.getElementById('singleUploadArea').querySelector('.upload-area').style.display = '';
document.getElementById('fileInput').value = '';
clearLog();
}
async function beginCheck() {
// Hide ready state, show progress
const readyDiv = document.getElementById('uploadReadyState');
if (readyDiv) readyDiv.style.display = 'none';
document.getElementById('progressContainer').style.display = 'block';
updateProgress(25, 'Initializing accessibility check...');
addLog('Preparing accessibility analysis...', 'info');
const quickMode = document.getElementById('quickMode').checked;
if (quickMode) addLog('Quick mode enabled — skipping expensive checks', 'info');
try {
updateProgress(30, 'Starting analysis...');
const result = await startCheck(currentJobId, quickMode);
if (result.success) {
updateProgress(35, 'Analysis queued');
addLog('Job queued for processing', 'success');
pollJobStatus();
} else {
addLog('Check failed: ' + result.error, 'error');
alert('Check failed: ' + result.error);
document.getElementById('progressContainer').style.display = 'none';
}
} catch (error) {
addLog('Check error: ' + error.message, 'error');
alert('Check failed: ' + error.message);
document.getElementById('progressContainer').style.display = 'none';
}
}
async function pollJobStatus() {
pollCount = 0;
const simStages = [
{ percent: 40, message: 'Loading PDF...', log: 'Reading PDF structure and metadata' },
{ percent: 50, message: 'Analyzing document structure...', log: 'Checking PDF tagging and structure' },
{ percent: 60, message: 'Analyzing images...', log: 'Processing images with AI' },
{ percent: 70, message: 'Checking color contrast...', log: 'Calculating WCAG contrast ratios' },
{ percent: 80, message: 'Analyzing readability...', log: 'Computing readability scores' },
{ percent: 90, message: 'Running final checks...', log: 'Font embedding, bookmarks, headings, tab order' },
{ percent: 95, message: 'Compiling results...', log: 'Generating accessibility report' }
];
let stageIdx = 0;
const tick = async () => {
pollCount++;
try {
const result = await checkStatus(currentJobId);
if (result.success) {
const data = result.data;
// Use real progress from Redis if available
if (data.progress && data.progress > 0) {
updateProgress(data.progress, data.status_message || data.status);
} else if (stageIdx < simStages.length && pollCount % 3 === 0) {
const s = simStages[stageIdx];
updateProgress(s.percent, s.message);
addLog(s.log, 'info');
stageIdx++;
}
if (data.status === 'completed') {
clearInterval(pollInterval);
updateProgress(98, 'Loading results...');
addLog('Analysis complete! Loading results...', 'success');
loadResults();
} else if (data.status === 'failed' || data.status === 'error') {
clearInterval(pollInterval);
addLog('Analysis failed', 'error');
if (data.error_log) addLog('Error: ' + data.error_log.substring(0, 500), 'error');
document.getElementById('progressContainer').style.display = 'none';
alert('Analysis failed. Check the error log for details.');
} else if (pollCount > 150) {
clearInterval(pollInterval);
addLog('Analysis timed out after 5 minutes', 'error');
addLog('Try using Quick Mode for faster results', 'info');
document.getElementById('progressContainer').style.display = 'none';
}
}
} catch (error) {
console.error('Status check failed:', error);
addLog('Status check error (retrying...): ' + error.message, 'warning');
}
};
tick();
pollInterval = setInterval(tick, 2000);
}
async function loadResults() {
updateProgress(100, 'Complete!');
addLog('Fetching results from server...', 'info');
try {
const result = await getResult(currentJobId);
if (result.success) {
addLog('Results loaded — Score: ' + result.data.accessibility_score + '/100', 'success');
await new Promise(r => setTimeout(r, 800));
displayResults(result.data);
} else {
addLog('Failed to load results: ' + result.error, 'error');
}
} catch (error) {
addLog('Error loading results: ' + error.message, 'error');
}
}
function resetCheck() {
if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
if (batchPollInterval) { clearInterval(batchPollInterval); batchPollInterval = null; }
pollCount = 0;
document.getElementById('uploadSection').style.display = 'block';
document.getElementById('resultsSection').style.display = 'none';
document.getElementById('progressContainer').style.display = 'none';
document.getElementById('pageViewerCard').style.display = 'none';
document.getElementById('fileInput').value = '';
var readyDiv = document.getElementById('uploadReadyState');
if (readyDiv) readyDiv.style.display = 'none';
var uploadArea = document.getElementById('singleUploadArea') && document.getElementById('singleUploadArea').querySelector('.upload-area');
if (uploadArea) uploadArea.style.display = '';
var remCard = document.getElementById('remediationCard');
if (remCard) remCard.style.display = 'none';
currentJobId = null;
clearLog();
}