- 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>
262 lines
9.2 KiB
JavaScript
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">🗑</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, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
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();
|
|
});
|