adobe-to-docusign-migrator/web/static/js/history.js

222 lines
8.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 = `<div class="empty-state"><div class="spinner"></div></div>`;
try {
const data = await api.migrate.history();
_allRecords = (data.history || []).reverse(); // newest first
} catch (e) {
outlet.innerHTML = `<div class="callout error"><span class="callout-icon">❌</span>Failed to load history: ${escHtml(e.message)}</div>`;
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 = `
<div class="page-header">
<div>
<div class="page-title">History &amp; Audit</div>
<div class="page-subtitle">${_allRecords.length} total migration records</div>
</div>
<div class="page-actions">
<button class="btn btn-secondary btn-sm" id="btn-export-history">⬇ Export CSV</button>
</div>
</div>
<!-- Filter bar -->
<div class="filter-bar">
<input type="search" class="search-input" id="hist-search"
placeholder="Search by template name…" value="${escHtml(_filter.search)}" />
<div class="filter-tabs">
<button class="filter-tab ${_filter.status === 'all' ? 'active' : ''}" data-status="all">All</button>
<button class="filter-tab ${_filter.status === 'success' ? 'active' : ''}" data-status="success">Success</button>
<button class="filter-tab ${_filter.status === 'error' ? 'active' : ''}" data-status="error">Errors</button>
<button class="filter-tab ${_filter.status === 'dry_run' ? 'active' : ''}" data-status="dry_run">Dry Run</button>
<button class="filter-tab ${_filter.status === 'skipped' ? 'active' : ''}" data-status="skipped">Skipped</button>
</div>
<div class="date-filter">
<label style="font-size:11px;color:var(--text-muted)">From:</label>
<input type="date" class="date-input" id="hist-from" value="${_filter.from}" />
<label style="font-size:11px;color:var(--text-muted)">To:</label>
<input type="date" class="date-input" id="hist-to" value="${_filter.to}" />
</div>
</div>
${filtered.length === 0 ? `
<div class="empty-state">
<div class="empty-state-icon">📋</div>
<div class="empty-state-title">${_allRecords.length ? 'No records match your filter' : 'No migration history yet'}</div>
<div class="empty-state-sub">${_allRecords.length ? 'Try clearing the search or filters.' : 'Run a migration to see history here.'}</div>
</div>
` : `
<div class="card">
<div class="table-wrap">
<table>
<thead>
<tr>
${_th('timestamp', 'Time')}
${_th('adobe_template_name', 'Template')}
${_th('action', 'Action')}
${_th('status', 'Status')}
<th>Docusign ID</th>
<th>Checksum</th>
</tr>
</thead>
<tbody>
${page.map(r => _historyRow(r)).join('')}
</tbody>
</table>
</div>
${totalPages > 1 ? `
<div class="pagination">
<span>Showing ${_page * PAGE_SIZE + 1}${Math.min((_page + 1) * PAGE_SIZE, filtered.length)} of ${filtered.length}</span>
<div class="pagination-pages">
<button class="page-btn" id="pg-prev" ${_page === 0 ? 'disabled' : ''}> Prev</button>
<button class="page-btn" id="pg-next" ${_page >= totalPages - 1 ? 'disabled' : ''}>Next </button>
</div>
</div>` : ''}
</div>
`}
`;
_bindEvents(filtered);
}
function _th(col, label) {
const dir = _sort.col === col ? (_sort.dir === 'asc' ? 'sort-asc' : 'sort-desc') : '';
return `<th class="sortable ${dir}" data-col="${col}">${label}</th>`;
}
function _historyRow(r) {
const statusBadge = r.status === 'success'
? `<span class="badge badge-green">${escHtml(r.action || 'success')}</span>`
: `<span class="badge badge-${r.status === 'skipped' ? 'gray' : r.status === 'dry_run' ? 'gray' : 'red'}">${escHtml(r.status || '—')}</span>`;
const checksum = r.checksum_sha256 || r.checksum || '';
return `
<tr class="row-expandable" data-expanded="false">
<td style="white-space:nowrap;font-size:12px">${(r.timestamp||'').slice(0,19).replace('T',' ')}</td>
<td>
<div class="table-name">${escHtml(r.adobe_template_name || r.adobe_template_id || '—')}</div>
<div class="table-sub">${escHtml(r.adobe_template_id || '')}</div>
</td>
<td>${escHtml(r.action || '—')}</td>
<td>${statusBadge}</td>
<td class="mono" style="font-size:11px">${r.docusign_template_id ? escHtml(r.docusign_template_id.slice(0,12)) + '…' : '—'}</td>
<td>
${checksum
? `<span class="checksum" title="${escHtml(checksum)}">${escHtml(shortHash(checksum))}</span>`
: '<span style="color:var(--text-muted)">—</span>'}
</td>
</tr>
${(r.blockers || r.warnings || r.error) ? `
<tr class="row-expanded-content" style="display:none">
<td colspan="6">
<div class="row-expand-body">
${(r.blockers||[]).map(b => `<div style="color:var(--error);font-size:12px">🚫 ${escHtml(b)}</div>`).join('')}
${(r.warnings||[]).map(w => `<div style="color:var(--warning);font-size:12px">⚠ ${escHtml(w)}</div>`).join('')}
${r.error ? `<div style="color:var(--error);font-size:12px">❌ ${escHtml(r.error)}</div>` : ''}
</div>
</td>
</tr>` : ''}
`;
}
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('; '),
})));
});
}