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>
219 lines
8.7 KiB
JavaScript
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();
|
|
}
|