(function() { const app = document.getElementById('app'); // Router function route() { const path = window.location.pathname; if (path === '/' || path === '/sessions') { renderSessions(); } else if (path.startsWith('/sessions/')) { const sessionID = path.split('/sessions/')[1]; renderSession(sessionID); } else if (path.startsWith('/runs/')) { const runID = path.split('/runs/')[1]; renderRun(runID); } else { app.innerHTML = '

Page not found

'; } } function navigate(path) { history.pushState(null, '', path); route(); } window.addEventListener('popstate', route); // API helpers async function api(path) { const resp = await fetch('/api' + path); if (!resp.ok) throw new Error('API error'); return resp.json(); } function relativeTime(ts) { const now = Date.now(); const then = new Date(ts).getTime(); const diff = now - then; if (diff < 60000) return 'just now'; if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago'; if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago'; return Math.floor(diff / 86400000) + 'd ago'; } function formatDuration(ms) { if (!ms) return '-'; if (ms < 1000) return ms + 'ms'; if (ms < 60000) return (ms / 1000).toFixed(1) + 's'; return (ms / 60000).toFixed(1) + 'm'; } function statusIcon(status) { if (status === 'success') return ''; if (status === 'error') return ''; return ''; } // Sessions list let sessionsState = { sessions: [], cursor: null, filters: {} }; async function renderSessions() { app.innerHTML = `
Session Framework Host Runs Time
`; // Bind filter events ['from', 'to', 'framework', 'host'].forEach(f => { document.getElementById('filter-' + f).addEventListener('change', () => { sessionsState.sessions = []; sessionsState.cursor = null; loadSessions(); }); }); document.getElementById('load-more').addEventListener('click', loadSessions); sessionsState = { sessions: [], cursor: null }; await loadSessions(); } async function loadSessions() { const params = new URLSearchParams(); const from = document.getElementById('filter-from').value; const to = document.getElementById('filter-to').value; const framework = document.getElementById('filter-framework').value; const host = document.getElementById('filter-host').value; if (from) params.set('from', from); if (to) params.set('to', to); if (framework) params.set('framework', framework); if (host) params.set('host', host); if (sessionsState.cursor) params.set('cursor', sessionsState.cursor); const data = await api('/v1/sessions?' + params.toString()); sessionsState.sessions = sessionsState.sessions.concat(data.sessions || []); sessionsState.cursor = data.next_cursor; const tbody = document.getElementById('sessions-body'); tbody.innerHTML = sessionsState.sessions.map(s => ` ${s.session_id.substring(0, 12)}... ${s.framework || '-'} ${s.host || '-'} ${s.run_count} ${relativeTime(s.started_at)} `).join('') || 'No sessions found'; tbody.querySelectorAll('tr.clickable').forEach(row => { row.addEventListener('click', () => navigate('/sessions/' + row.dataset.session)); }); document.getElementById('load-more').style.display = sessionsState.cursor ? 'block' : 'none'; } // Session detail async function renderSession(sessionID) { const data = await api('/v1/sessions/' + sessionID); const s = data.session; const runs = data.runs || []; const duration = s.ended_at ? formatDuration(new Date(s.ended_at) - new Date(s.started_at)) : 'ongoing'; app.innerHTML = ` ← Back to Sessions

Runs (${runs.length})

${runs.map(r => { const dur = r.ended_at ? formatDuration(new Date(r.ended_at) - new Date(r.started_at)) : '-'; return ` `; }).join('') || ''}
Run ID Status Spans Duration Started
${r.run_id.substring(0, 12)}... ${statusIcon(r.status)} ${r.status} ${r.span_count} ${dur} ${new Date(r.started_at).toLocaleTimeString()}
No runs
`; document.querySelectorAll('tr.clickable').forEach(row => { row.addEventListener('click', () => navigate('/runs/' + row.dataset.run)); }); document.querySelector('.back-link').addEventListener('click', e => { e.preventDefault(); navigate('/sessions'); }); } // Run detail async function renderRun(runID) { const data = await api('/v1/runs/' + runID); const r = data.run; const spans = data.spans || []; const duration = r.ended_at ? formatDuration(new Date(r.ended_at) - new Date(r.started_at)) : 'ongoing'; app.innerHTML = ` ← Back to Session

Spans (${spans.length})

${spans.map((sp, i) => ` `).join('') || ''}
Name Kind Status Duration
No spans
`; document.querySelectorAll('tr.expandable').forEach(row => { row.addEventListener('click', () => { const idx = row.dataset.index; const detailRow = document.querySelector(`tr.span-detail-row[data-index="${idx}"]`); const icon = row.querySelector('.expand-icon'); if (detailRow.style.display === 'none') { detailRow.style.display = 'table-row'; icon.innerHTML = '▼'; } else { detailRow.style.display = 'none'; icon.innerHTML = '▶'; } }); }); document.querySelector('.back-link').addEventListener('click', e => { e.preventDefault(); navigate('/sessions/' + r.session_id); }); } // Start route(); })();