135 lines
4.8 KiB
JavaScript
135 lines
4.8 KiB
JavaScript
// Recent activity view for tester/admin auditing
|
|
|
|
import { api } from './api.js';
|
|
import { state } from './state.js';
|
|
import { escHtml, formatDateTime } from './utils.js';
|
|
|
|
const ACTION_LABELS = {
|
|
adobe_connected: 'Adobe connected',
|
|
adobe_disconnected: 'Adobe disconnected',
|
|
docusign_authorization_requested: 'DocuSign auth requested',
|
|
docusign_authorization_started: 'DocuSign auth started',
|
|
docusign_connected: 'DocuSign connected',
|
|
docusign_account_selected: 'DocuSign account selected',
|
|
docusign_disconnected: 'DocuSign disconnected',
|
|
migration_run: 'Migration run',
|
|
migration_batch_started: 'Batch migration started',
|
|
migration_batch_completed: 'Batch migration completed',
|
|
verification_sent: 'Verification sent',
|
|
verification_voided: 'Verification voided',
|
|
};
|
|
const ACTIVITY_SCOPE_KEY = 'migrator_activity_scope';
|
|
|
|
export async function renderActivity() {
|
|
const outlet = document.getElementById('router-outlet');
|
|
outlet.innerHTML = `<div class="empty-state"><div class="spinner"></div></div>`;
|
|
const showAll = state.auth.isAdmin && localStorage.getItem(ACTIVITY_SCOPE_KEY) === 'all';
|
|
|
|
try {
|
|
const data = await api.audit.recent(150, showAll);
|
|
const events = data.events || [];
|
|
const isAdmin = !!data.is_admin;
|
|
const scope = data.scope || 'session';
|
|
|
|
outlet.innerHTML = `
|
|
<div class="page-header">
|
|
<div>
|
|
<div class="page-title">Recent Activity</div>
|
|
<div class="page-subtitle">${scope === 'all'
|
|
? 'Admin view of all tester activity.'
|
|
: 'Your activity in this browser session.'}</div>
|
|
</div>
|
|
${isAdmin ? `
|
|
<div class="page-actions">
|
|
<button class="btn btn-secondary btn-sm" id="btn-toggle-activity-scope">${scope === 'all' ? 'Show My Activity' : 'Show All Activity'}</button>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
|
|
${events.length === 0 ? `
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">🧾</div>
|
|
<div class="empty-state-title">No activity yet</div>
|
|
<div class="empty-state-sub">Recent tester actions will appear here after people connect and use the app.</div>
|
|
</div>
|
|
` : `
|
|
<div class="card">
|
|
<div class="table-wrap">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Time</th>
|
|
<th>Action</th>
|
|
<th>DocuSign</th>
|
|
<th>Adobe</th>
|
|
<th>Session</th>
|
|
<th>IP</th>
|
|
<th>Details</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${events.map(renderEventRow).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
`}
|
|
`;
|
|
|
|
document.getElementById('btn-toggle-activity-scope')?.addEventListener('click', () => {
|
|
localStorage.setItem(ACTIVITY_SCOPE_KEY, scope === 'all' ? 'session' : 'all');
|
|
renderActivity();
|
|
});
|
|
} catch (e) {
|
|
outlet.innerHTML = `<div class="callout error"><span class="callout-icon">❌</span>Failed to load activity: ${escHtml(e.message)}</div>`;
|
|
}
|
|
}
|
|
|
|
function renderEventRow(event) {
|
|
const docusignLabel = event.docusign_account_name || event.docusign_user_name || event.docusign_user_email || '—';
|
|
const adobeLabel = event.adobe_account_name || event.adobe_user_name || event.adobe_user_email || '—';
|
|
const sessionId = event.session_id ? `${event.session_id.slice(0, 10)}…` : '—';
|
|
const detailText = formatDetails(event.details);
|
|
|
|
return `
|
|
<tr>
|
|
<td style="white-space: nowrap">${escHtml(formatDateTime(event.timestamp))}</td>
|
|
<td><span class="badge badge-blue">${escHtml(ACTION_LABELS[event.action] || event.action || 'Activity')}</span></td>
|
|
<td>
|
|
<div class="table-name">${escHtml(docusignLabel)}</div>
|
|
<div class="table-sub">${escHtml(event.docusign_account_id || event.docusign_user_email || '')}</div>
|
|
</td>
|
|
<td>
|
|
<div class="table-name">${escHtml(adobeLabel)}</div>
|
|
<div class="table-sub">${escHtml(event.adobe_account_id || event.adobe_user_email || '')}</div>
|
|
</td>
|
|
<td class="mono">${escHtml(sessionId)}</td>
|
|
<td>${escHtml(event.ip || '—')}</td>
|
|
<td class="activity-details">${escHtml(detailText)}</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
|
|
function formatDetails(details) {
|
|
if (!details || typeof details !== 'object') {
|
|
return '—';
|
|
}
|
|
|
|
const parts = Object.entries(details)
|
|
.filter(([, value]) => value !== null && value !== undefined && value !== '')
|
|
.map(([key, value]) => `${humanizeKey(key)}: ${formatValue(value)}`);
|
|
|
|
return parts.length ? parts.join(' | ') : '—';
|
|
}
|
|
|
|
function humanizeKey(key) {
|
|
return key.replace(/_/g, ' ');
|
|
}
|
|
|
|
function formatValue(value) {
|
|
if (typeof value === 'boolean') {
|
|
return value ? 'yes' : 'no';
|
|
}
|
|
return String(value);
|
|
}
|