pdf-accessibility/js/app.js
Vadym Samoilenko 1126c8a700 Fix history: correct score field name + add delete button
- api.php: read accessibility_score (not score) from result.json
- api.php: handleDelete() also removes .dismissed.json, .overrides.json, .error.log
- js/app.js: add Delete button to each history row with confirm dialog
- css/styles.css: red hover style for delete button

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

262 lines
9.2 KiB
JavaScript

/* App initialization and MSAL authentication */
// MSAL configuration
const msalConfig = {
auth: {
clientId: '', // Set from data attribute or env
authority: '',
redirectUri: window.location.origin + window.location.pathname
},
cache: {
cacheLocation: 'localStorage',
storeAuthStateInCookie: false
}
};
let msalInstance = null;
window.msalToken = null;
function initMsal() {
const el = document.getElementById('msalConfig');
if (!el) return;
const tenantId = el.dataset.tenantId;
const clientId = el.dataset.clientId;
const redirectUri = el.dataset.redirectUri;
if (!tenantId || !clientId) return;
msalConfig.auth.clientId = clientId;
msalConfig.auth.authority = `https://login.microsoftonline.com/${tenantId}`;
if (redirectUri) msalConfig.auth.redirectUri = redirectUri;
// Load MSAL library dynamically
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/@azure/msal-browser@2/lib/msal-browser.min.js';
script.onload = () => {
msalInstance = new msal.PublicClientApplication(msalConfig);
msalInstance.initialize().then(() => {
handleMsalRedirect();
});
};
document.head.appendChild(script);
}
async function handleMsalRedirect() {
try {
const response = await msalInstance.handleRedirectPromise();
if (response) {
window.msalToken = response.accessToken;
showAuthenticatedUI(response.account);
return;
}
} catch (e) {
console.error('MSAL redirect error:', e);
}
// Check for existing session
const accounts = msalInstance.getAllAccounts();
if (accounts.length > 0) {
try {
const tokenResponse = await msalInstance.acquireTokenSilent({
scopes: ['User.Read'],
account: accounts[0]
});
window.msalToken = tokenResponse.accessToken;
showAuthenticatedUI(accounts[0]);
} catch (e) {
// Token expired, show login
showLoginUI();
}
} else {
// Check if we're in dev mode (localhost) — skip MSAL
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
hideAuthOverlay();
} else {
showLoginUI();
}
}
}
function showLoginUI() {
const overlay = document.getElementById('authOverlay');
if (overlay) overlay.classList.add('active');
}
function hideAuthOverlay() {
const overlay = document.getElementById('authOverlay');
if (overlay) overlay.classList.remove('active');
}
function showAuthenticatedUI(account) {
hideAuthOverlay();
const userInfo = document.getElementById('userInfo');
if (userInfo && account) {
userInfo.textContent = account.name || account.username;
}
const logoutBtn = document.getElementById('logoutBtn');
if (logoutBtn) logoutBtn.style.display = 'inline-block';
// Show history section and load jobs
const historySection = document.getElementById('historySection');
if (historySection) historySection.style.display = '';
loadHistory();
}
// ─── Document History ────────────────────────────────────────────────────────
async function loadHistory() {
const wrap = document.getElementById('historyTableWrap');
if (!wrap) return;
try {
const data = await apiCall('list');
renderHistory(data.jobs || []);
} catch (e) {
console.error('Failed to load history:', e);
}
}
function renderHistory(jobs) {
const wrap = document.getElementById('historyTableWrap');
const empty = document.getElementById('historyEmpty');
if (!jobs.length) {
if (empty) empty.style.display = '';
// Remove table if it existed
const old = wrap.querySelector('table');
if (old) old.remove();
return;
}
if (empty) empty.style.display = 'none';
const rows = jobs.map(j => {
const score = j.score != null ? j.score : '—';
const grade = j.grade || '—';
const scoreClass = j.score >= 90 ? 'history-score-a'
: j.score >= 70 ? 'history-score-b'
: j.score != null ? 'history-score-f' : '';
const status = j.status === 'completed' ? '<span class="history-badge-done">Done</span>'
: '<span class="history-badge-pending">Pending</span>';
const critical = j.critical_count ?? 0;
const errors = j.error_count ?? 0;
const date = j.uploaded_at ? j.uploaded_at.replace('T', ' ').substring(0, 16) : '—';
const name = escapeHtml(j.original_filename || j.job_id);
const openBtn = j.status === 'completed'
? `<button class="history-action-btn" onclick="openHistoryJob('${j.job_id}')" title="Open report">Open</button>`
: '';
const htmlBtn = j.status === 'completed'
? `<a class="history-action-btn" href="api.php?action=export&job_id=${j.job_id}&format=html" target="_blank" title="Download HTML report">HTML</a>`
: '';
const pdfBtn = j.status === 'completed'
? `<a class="history-action-btn" href="api.php?action=export&job_id=${j.job_id}&format=pdf" target="_blank" title="Download PDF report">PDF</a>`
: '';
const jsonBtn = j.status === 'completed'
? `<a class="history-action-btn" href="api.php?action=export&job_id=${j.job_id}&format=json" target="_blank" title="Download JSON">JSON</a>`
: '';
const deleteBtn = `<button class="history-action-btn history-action-delete" onclick="deleteHistoryJob('${j.job_id}', this)" title="Delete">&#x1F5D1;</button>`;
return `<tr>
<td class="history-filename" title="${escapeHtml(j.original_filename || '')}">${name}</td>
<td>${date}</td>
<td>${status}</td>
<td><span class="history-score ${scoreClass}">${score}${j.score != null ? '<small>/100</small>' : ''}</span> <span class="history-grade">${grade}</span></td>
<td>${critical > 0 ? `<span class="history-crit">${critical} crit</span>` : ''} ${errors > 0 ? `<span class="history-err">${errors} err</span>` : ''}${!critical && !errors ? '<span style="color:var(--success)">✓ Clean</span>' : ''}</td>
<td class="history-actions">${openBtn}${htmlBtn}${pdfBtn}${jsonBtn}${deleteBtn}</td>
</tr>`;
}).join('');
// Replace or create table
const old = wrap.querySelector('table');
if (old) old.remove();
const table = document.createElement('table');
table.className = 'history-table';
table.setAttribute('aria-label', 'Document history');
table.innerHTML = `
<thead><tr>
<th>Document</th>
<th>Date</th>
<th>Status</th>
<th>Score</th>
<th>Issues</th>
<th>Actions</th>
</tr></thead>
<tbody>${rows}</tbody>`;
wrap.appendChild(table);
}
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
async function deleteHistoryJob(jobId, btn) {
if (!confirm('Delete this document and its report?')) return;
btn.disabled = true;
try {
const formData = new FormData();
formData.append('job_id', jobId);
const data = await apiCall('delete', { method: 'POST', body: formData });
if (data.success) {
btn.closest('tr').remove();
// Show empty state if no rows left
const tbody = document.querySelector('.history-table tbody');
if (tbody && !tbody.querySelector('tr')) {
document.querySelector('.history-table').remove();
const empty = document.getElementById('historyEmpty');
if (empty) empty.style.display = '';
}
} else {
alert('Delete failed: ' + (data.error || 'Unknown error'));
btn.disabled = false;
}
} catch (e) {
alert('Delete failed.');
btn.disabled = false;
}
}
async function openHistoryJob(jobId) {
// Show results section and load the job — reuse the existing display flow
currentJobId = jobId;
const resultsSection = document.getElementById('resultsSection');
if (resultsSection) resultsSection.style.display = '';
try {
const data = await getResult(jobId);
if (data.error) { alert('Could not load report: ' + data.error); return; }
displayResults(data);
resultsSection.scrollIntoView({ behavior: 'smooth' });
} catch (e) {
alert('Failed to load report.');
}
}
async function loginWithMicrosoft() {
if (!msalInstance) return;
try {
await msalInstance.loginRedirect({ scopes: ['User.Read'] });
} catch (e) {
console.error('Login failed:', e);
alert('Login failed. Please try again.');
}
}
function logout() {
if (msalInstance) {
msalInstance.logoutRedirect();
}
}
/* App init */
document.addEventListener('DOMContentLoaded', () => {
loadTheme();
initUpload();
initBatchUpload();
initMsal();
});