Move document history to separate history.html page

- history.html: standalone page with My Documents table + auth
- js/history.js: renderHistory, loadHistory, deleteHistoryJob logic
- js/app-history.js: MSAL auth init for history.html
- index.html: remove history section, add 'My Documents' link in header
- js/app.js: show historyLink after auth, open job from ?job_id= URL param
- deploy.sh: include history.html in deploy

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-03-13 15:21:19 +00:00
parent 373bf88a29
commit 62094f4dfa
6 changed files with 304 additions and 134 deletions

View file

@ -158,12 +158,13 @@ sudo mkdir -p "${WEB_DIR}"
# Clean old frontend files (but preserve uploads, results, .env, logs)
log "Cleaning old frontend files..."
sudo rm -f "${WEB_DIR}/index.html"
sudo rm -f "${WEB_DIR}/index.html" "${WEB_DIR}/history.html"
sudo rm -rf "${WEB_DIR}/css" "${WEB_DIR}/js"
sudo rm -f "${WEB_DIR}/api.php" "${WEB_DIR}/auth.php"
# Copy frontend files
sudo cp "${REPO_DIR}/index.html" "${WEB_DIR}/"
sudo cp "${REPO_DIR}/history.html" "${WEB_DIR}/"
sudo cp -r "${REPO_DIR}/css" "${WEB_DIR}/"
sudo cp -r "${REPO_DIR}/js" "${WEB_DIR}/"

68
history.html Normal file
View file

@ -0,0 +1,68 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Documents — PDF Accessibility Checker</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<div id="msalConfig" hidden
data-tenant-id="e519c2e6-bc6d-4fdf-8d9c-923c2f002385"
data-client-id="9079054c-9620-4757-a256-23413042f1ef"
data-redirect-uri="https://ai-sandbox.oliver.solutions/pdf-accessibility/history.html"></div>
<!-- Auth Overlay -->
<div class="auth-overlay" id="authOverlay" role="dialog" aria-label="Sign in required" aria-modal="true" aria-describedby="authCardDesc">
<div class="auth-card">
<h2>PDF Accessibility Checker</h2>
<p id="authCardDesc">Sign in with your organization account to continue.</p>
<button class="btn-microsoft" onclick="loginWithMicrosoft()" aria-label="Sign in with Microsoft">
<svg width="20" height="20" viewBox="0 0 21 21" aria-hidden="true"><rect x="1" y="1" width="9" height="9" fill="#f25022"/><rect x="11" y="1" width="9" height="9" fill="#7fba00"/><rect x="1" y="11" width="9" height="9" fill="#00a4ef"/><rect x="11" y="11" width="9" height="9" fill="#ffb900"/></svg>
Sign in with Microsoft
</button>
</div>
</div>
<header>
<div class="container">
<div class="header-inner">
<div>
<h1>Enterprise PDF Accessibility Checker</h1>
<p class="subtitle">Comprehensive WCAG 2.1 compliance validation with AI-powered analysis</p>
</div>
<div class="header-actions">
<a href="index.html" class="btn btn-secondary" style="text-decoration:none;padding:8px 16px;font-size:13px;">&#x2B06; New Check</a>
<span class="user-info" id="userInfo"></span>
<button id="logoutBtn" onclick="logout()" style="display:none;">Sign Out</button>
<button id="themeToggle" onclick="toggleDarkMode()" aria-label="Toggle dark mode">Dark</button>
</div>
</div>
</div>
</header>
<main id="main-content">
<div class="container" style="padding-top: 32px;">
<div class="card" id="historySection" style="display:none;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;">
<h2 style="margin:0;">My Documents</h2>
<button class="btn btn-secondary" onclick="loadHistory()" aria-label="Refresh" style="padding:8px 16px;font-size:13px;">&#x21BA; Refresh</button>
</div>
<div id="historyTableWrap">
<p style="color:var(--text-muted);font-size:14px;" id="historyEmpty">No documents checked yet. <a href="index.html">Upload a PDF</a> to get started.</p>
</div>
</div>
</div>
</main>
<script src="js/utils.js"></script>
<script src="js/api.js"></script>
<script src="js/history.js"></script>
<script src="js/app-history.js"></script>
</body>
</html>

View file

@ -38,6 +38,7 @@
<p class="subtitle">Comprehensive WCAG 2.1 compliance validation with AI-powered analysis</p>
</div>
<div class="header-actions">
<a href="history.html" id="historyLink" style="display:none;text-decoration:none;" class="btn btn-secondary" style="padding:8px 16px;font-size:13px;">&#x1F4C2; My Documents</a>
<span class="user-info" id="userInfo"></span>
<button id="logoutBtn" onclick="logout()" style="display:none;">Sign Out</button>
<button id="themeToggle" onclick="toggleDarkMode()" aria-label="Toggle dark mode">Dark</button>
@ -48,17 +49,6 @@
<main id="main-content">
<div class="container">
<!-- History Section (shown when authenticated) -->
<div class="card" id="historySection" style="display:none;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;">
<h2 style="margin:0;">My Documents</h2>
<button class="btn btn-secondary" onclick="loadHistory()" id="historyRefreshBtn" aria-label="Refresh document history" style="padding:8px 16px;font-size:13px;">&#x21BA; Refresh</button>
</div>
<div id="historyTableWrap">
<p style="color:var(--text-muted);font-size:14px;" id="historyEmpty">No documents checked yet. Upload a PDF to get started.</p>
</div>
</div>
<!-- Upload Section -->
<div class="card" id="uploadSection">
<h2>Upload PDF Document</h2>

110
js/app-history.js Normal file
View file

@ -0,0 +1,110 @@
/* MSAL auth + init for history.html */
const msalConfig = {
auth: {
clientId: '',
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;
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); }
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) { showLoginUI(); }
} else {
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
showAuthenticatedUI(null);
} else {
showLoginUI();
}
}
}
function showLoginUI() {
const overlay = document.getElementById('authOverlay');
if (overlay) overlay.classList.add('active');
}
function showAuthenticatedUI(account) {
const overlay = document.getElementById('authOverlay');
if (overlay) overlay.classList.remove('active');
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';
const historySection = document.getElementById('historySection');
if (historySection) historySection.style.display = '';
loadHistory();
}
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();
}
function toggleDarkMode() {
const isDark = document.body.classList.toggle('dark-mode');
localStorage.setItem('darkMode', isDark ? '1' : '0');
document.getElementById('themeToggle').textContent = isDark ? 'Light' : 'Dark';
}
function loadTheme() {
if (localStorage.getItem('darkMode') === '1') {
document.body.classList.add('dark-mode');
const btn = document.getElementById('themeToggle');
if (btn) btn.textContent = 'Light';
}
}
document.addEventListener('DOMContentLoaded', () => {
loadTheme();
initMsal();
});

129
js/app.js
View file

@ -97,129 +97,14 @@ function showAuthenticatedUI(account) {
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();
}
// Show My Documents link in header
const historyLink = document.getElementById('historyLink');
if (historyLink) historyLink.style.display = 'inline-block';
// ─── Document History ────────────────────────────────────────────────────────
async function loadHistory() {
const wrap = document.getElementById('historyTableWrap');
if (!wrap) return;
try {
const data = await apiCall('list');
const jobs = data?.data?.jobs || data?.jobs || [];
renderHistory(jobs);
} catch (e) {
console.error('[history] failed to load:', 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;
}
// If URL has ?job_id= open that report directly
const params = new URLSearchParams(window.location.search);
const jobId = params.get('job_id');
if (jobId) openHistoryJob(jobId);
}
async function openHistoryJob(jobId) {

116
js/history.js Normal file
View file

@ -0,0 +1,116 @@
/* Document history table — used on history.html */
async function loadHistory() {
const wrap = document.getElementById('historyTableWrap');
if (!wrap) return;
try {
const data = await apiCall('list');
const jobs = data?.data?.jobs || data?.jobs || [];
renderHistory(jobs);
} catch (e) {
console.error('[history] failed to load:', e);
}
}
function renderHistory(jobs) {
const wrap = document.getElementById('historyTableWrap');
const empty = document.getElementById('historyEmpty');
if (!jobs.length) {
if (empty) empty.style.display = '';
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'
? `<a class="history-action-btn" href="index.html?job_id=${j.job_id}" title="Open report">Open</a>`
: '';
const htmlBtn = j.status === 'completed'
? `<a class="history-action-btn" href="api.php?action=export&job_id=${j.job_id}&format=html" target="_blank">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">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">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 && j.status === 'completed' ? '<span style="color:var(--success)">✓ Clean</span>' : ''}</td>
<td class="history-actions">${openBtn}${htmlBtn}${pdfBtn}${jsonBtn}${deleteBtn}</td>
</tr>`;
}).join('');
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();
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;
}
}