diff --git a/web/static/js/history.js b/web/static/js/history.js new file mode 100644 index 0000000..daa670a --- /dev/null +++ b/web/static/js/history.js @@ -0,0 +1,221 @@ +// History & Audit view β€” filterable, exportable migration history + +import { api } from './api.js'; +import { escHtml, formatDateTime, shortHash, downloadCsv, debounce } from './utils.js'; + +let _allRecords = []; +let _filter = { search: '', status: 'all', from: '', to: '' }; +let _sort = { col: 'timestamp', dir: 'desc' }; +const PAGE_SIZE = 50; +let _page = 0; + +export async function renderHistory() { + const outlet = document.getElementById('router-outlet'); + outlet.innerHTML = `
`; + + try { + const data = await api.migrate.history(); + _allRecords = (data.history || []).reverse(); // newest first + } catch (e) { + outlet.innerHTML = `
❌Failed to load history: ${escHtml(e.message)}
`; + return; + } + + _page = 0; + _render(); +} + +function _render() { + const outlet = document.getElementById('router-outlet'); + const filtered = _applyFilter(_allRecords); + const page = filtered.slice(_page * PAGE_SIZE, (_page + 1) * PAGE_SIZE); + const totalPages = Math.ceil(filtered.length / PAGE_SIZE); + + outlet.innerHTML = ` + + + +
+ +
+ + + + + +
+
+ + + + +
+
+ + ${filtered.length === 0 ? ` +
+
πŸ“‹
+
${_allRecords.length ? 'No records match your filter' : 'No migration history yet'}
+
${_allRecords.length ? 'Try clearing the search or filters.' : 'Run a migration to see history here.'}
+
+ ` : ` +
+
+ + + + ${_th('timestamp', 'Time')} + ${_th('adobe_template_name', 'Template')} + ${_th('action', 'Action')} + ${_th('status', 'Status')} + + + + + + ${page.map(r => _historyRow(r)).join('')} + +
DocuSign IDChecksum
+
+ + ${totalPages > 1 ? ` + ` : ''} +
+ `} + `; + + _bindEvents(filtered); +} + +function _th(col, label) { + const dir = _sort.col === col ? (_sort.dir === 'asc' ? 'sort-asc' : 'sort-desc') : ''; + return `${label}`; +} + +function _historyRow(r) { + const statusBadge = r.status === 'success' + ? `${escHtml(r.action || 'success')}` + : `${escHtml(r.status || 'β€”')}`; + + const checksum = r.checksum_sha256 || r.checksum || ''; + + return ` + + ${(r.timestamp||'').slice(0,19).replace('T',' ')} + +
${escHtml(r.adobe_template_name || r.adobe_template_id || 'β€”')}
+
${escHtml(r.adobe_template_id || '')}
+ + ${escHtml(r.action || 'β€”')} + ${statusBadge} + ${r.docusign_template_id ? escHtml(r.docusign_template_id.slice(0,12)) + '…' : 'β€”'} + + ${checksum + ? `${escHtml(shortHash(checksum))}` + : 'β€”'} + + + ${(r.blockers || r.warnings || r.error) ? ` + + +
+ ${(r.blockers||[]).map(b => `
🚫 ${escHtml(b)}
`).join('')} + ${(r.warnings||[]).map(w => `
⚠ ${escHtml(w)}
`).join('')} + ${r.error ? `
❌ ${escHtml(r.error)}
` : ''} +
+ + ` : ''} + `; +} + +function _applyFilter(records) { + let list = [...records]; + + if (_filter.search) { + const q = _filter.search.toLowerCase(); + list = list.filter(r => + (r.adobe_template_name || '').toLowerCase().includes(q) || + (r.adobe_template_id || '').toLowerCase().includes(q) + ); + } + if (_filter.status !== 'all') { + list = list.filter(r => r.status === _filter.status); + } + if (_filter.from) { + list = list.filter(r => r.timestamp >= _filter.from); + } + if (_filter.to) { + list = list.filter(r => r.timestamp <= _filter.to + 'T23:59:59'); + } + + list.sort((a, b) => { + const va = a[_sort.col] || ''; + const vb = b[_sort.col] || ''; + return _sort.dir === 'asc' ? String(va).localeCompare(String(vb)) : String(vb).localeCompare(String(va)); + }); + + return list; +} + +function _bindEvents(filtered) { + document.getElementById('hist-search')?.addEventListener('input', debounce(e => { + _filter.search = e.target.value; _page = 0; _render(); + }, 250)); + + document.querySelectorAll('.filter-tab[data-status]').forEach(btn => { + btn.addEventListener('click', () => { _filter.status = btn.dataset.status; _page = 0; _render(); }); + }); + + document.getElementById('hist-from')?.addEventListener('change', e => { _filter.from = e.target.value; _page = 0; _render(); }); + document.getElementById('hist-to')?.addEventListener('change', e => { _filter.to = e.target.value; _page = 0; _render(); }); + + document.querySelectorAll('th.sortable').forEach(th => { + th.addEventListener('click', () => { + const col = th.dataset.col; + _sort.dir = _sort.col === col && _sort.dir === 'asc' ? 'desc' : 'asc'; + _sort.col = col; _page = 0; _render(); + }); + }); + + document.getElementById('pg-prev')?.addEventListener('click', () => { if (_page > 0) { _page--; _render(); } }); + document.getElementById('pg-next')?.addEventListener('click', () => { _page++; _render(); }); + + // Expand rows + document.querySelectorAll('.row-expandable').forEach(row => { + row.addEventListener('click', () => { + const next = row.nextElementSibling; + if (next && next.classList.contains('row-expanded-content')) { + const open = next.style.display !== 'none'; + next.style.display = open ? 'none' : 'table-row'; + } + }); + }); + + document.getElementById('btn-export-history')?.addEventListener('click', () => { + downloadCsv('migration-history.csv', filtered.map(r => ({ + timestamp: r.timestamp || '', + template: r.adobe_template_name || r.adobe_template_id || '', + adobe_id: r.adobe_template_id || '', + docusign_id: r.docusign_template_id || '', + action: r.action || '', + status: r.status || '', + checksum: r.checksum_sha256 || '', + warnings: (r.warnings || []).join('; '), + }))); + }); +}