diff --git a/cmd/web-ui/static/app.js b/cmd/web-ui/static/app.js index f463f3f..ce81f07 100644 --- a/cmd/web-ui/static/app.js +++ b/cmd/web-ui/static/app.js @@ -35,7 +35,75 @@ updateToggleBtn(getTheme()); const btn = document.getElementById('theme-toggle'); if (btn) btn.addEventListener('click', cycleTheme); + + // Global Search + const searchInput = document.getElementById('global-search'); + if (searchInput) { + searchInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + const val = searchInput.value.trim(); + if (val) handleGlobalSearch(val); + } + }); + } + + // Keyboard Shortcuts + document.addEventListener('keydown', (e) => { + // '/' to focus search + if (e.key === '/' && document.activeElement !== searchInput && !['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName)) { + e.preventDefault(); + searchInput.focus(); + } + }); }); + + async function handleGlobalSearch(id) { + if (id.length < 8) { + showToast('Search ID must be at least 8 characters', 'info'); + return; + } + + try { + // Try fetching as session first + const sessionData = await api('/v1/sessions/' + id).catch(() => null); + if (sessionData && sessionData.session) { + navigate('/sessions/' + id); + return; + } + + // Then try as run + const runData = await api('/v1/runs/' + id).catch(() => null); + if (runData && runData.run) { + navigate('/runs/' + id); + return; + } + + showToast('ID not found', 'error'); + } catch (e) { + showToast('Search failed: ' + e.message, 'error'); + } + } + + function copyToClipboard(text, el) { + if (!text) return; + navigator.clipboard.writeText(text).then(() => { + showToast('Copied to clipboard', 'success'); + if (el) { + const originalText = el.textContent; + el.textContent = 'Copied!'; + setTimeout(() => { el.textContent = originalText; }, 1500); + } + }).catch(err => { + console.error('Failed to copy:', err); + showToast('Copy failed', 'error'); + }); + } + + function renderCopyButton(text) { + return ``; + } // ───────────────────────────────────────────────────────── const app = document.getElementById('app'); @@ -184,6 +252,7 @@ function route() { cleanupLiveViews(); + renderBreadcrumbs(); const path = window.location.pathname; if (path === '/') { @@ -204,6 +273,44 @@ updateActiveNav(); } + function renderBreadcrumbs() { + const el = document.getElementById('breadcrumbs'); + if (!el) return; + + const path = window.location.pathname; + const parts = path.split('/').filter(Boolean); + if (parts.length === 0) { + el.innerHTML = ''; + return; + } + + const items = [{ label: 'Dashboard', path: '/' }]; + let currentPath = ''; + + parts.forEach((part, i) => { + currentPath += '/' + part; + let label = part.charAt(0).toUpperCase() + part.slice(1); + + // Special labels for IDs + if (part.length > 20 || /^[a-f0-9-]{32,}$/.test(part)) { + label = part.substring(0, 8) + '…'; + } + + if (i === parts.length - 1) { + items.push({ label, current: true }); + } else { + items.push({ label, path: currentPath }); + } + }); + + el.innerHTML = items.map(item => { + if (item.current) { + return `${escapeHTML(item.label)}`; + } + return `${escapeHTML(item.label)}/`; + }).join(''); + } + function navigate(path) { history.pushState(null, '', path); route(); @@ -293,8 +400,8 @@ } function statusIcon(status) { - if (status === 'success') return 'success'; - if (status === 'error') return 'error'; + if (status === 'success') return 'success'; + if (status === 'error') return 'error'; return 'unknown'; } @@ -445,7 +552,7 @@ : (active ? 'Open session' : 'Session ended'); return ` - ${escapeHTML(s.session_id.substring(0, 12))}… + ${escapeHTML(s.session_id.substring(0, 12))}…${renderCopyButton(s.session_id)} ${escapeHTML(fw)} ${escapeHTML(s.host || '-')} ${s.run_count} @@ -546,7 +653,7 @@ ? 'Currently active session' : (active ? 'Open session' : 'Session ended'); return ` - ${escapeHTML(s.session_id.substring(0, 12))}… + ${escapeHTML(s.session_id.substring(0, 12))}…${renderCopyButton(s.session_id)} ${escapeHTML(fw)} ${escapeHTML(s.host || '-')} ${s.run_count} @@ -668,7 +775,7 @@ app.innerHTML = ` ← Back to Sessions