diff options
| author | Ben Sima <ben@bensima.com> | 2025-12-19 21:54:54 -0500 |
|---|---|---|
| committer | Ben Sima <ben@bensima.com> | 2025-12-19 21:54:54 -0500 |
| commit | fcb8629182fa1552e4a840ccd4ec0aa2b8042cc0 (patch) | |
| tree | e389479cf9349fbdab107da739bceef11cf8e7ee /Omni/Ava/Web | |
| parent | e856c766584ed933bed0b79c7ef47b6d98b0fb7e (diff) | |
feat(ava): add tool trace viewer mini-app
- Add SQLite storage for tool traces (Omni/Ava/Trace.hs)
- Add web server to serve trace viewer (Omni/Ava/Web.hs)
- Add HTML/CSS/JS trace viewer UI (Omni/Ava/Web/trace.html)
- Integrate trace storage into Engine.hs tool execution callback
- Add trace links to Telegram responses when AVA_WEB_URL is set
- Configure Tailscale Funnel for public access
- Fix pre-push hook variable scope bug
- Add direnv, bash, nix to Ava service PATH
- Add mustache dep to Ava.hs for template rendering
Epic: t-272
Diffstat (limited to 'Omni/Ava/Web')
| -rw-r--r-- | Omni/Ava/Web/trace.html | 71 |
1 files changed, 71 insertions, 0 deletions
diff --git a/Omni/Ava/Web/trace.html b/Omni/Ava/Web/trace.html new file mode 100644 index 0000000..ce990a4 --- /dev/null +++ b/Omni/Ava/Web/trace.html @@ -0,0 +1,71 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <title>Trace: {{tool_name}}</title> + <style> + :root { --bg: #1a1a2e; --fg: #eee; --accent: #4a9eff; --code-bg: #0d0d1a; } + @media (prefers-color-scheme: light) { + :root { --bg: #fff; --fg: #222; --accent: #0066cc; --code-bg: #f5f5f5; } + } + * { box-sizing: border-box; } + body { font-family: system-ui, sans-serif; background: var(--bg); color: var(--fg); padding: 1rem; margin: 0; max-width: 100%; } + h1 { font-size: 1.25rem; margin: 0 0 0.5rem; } + .meta { font-size: 0.875rem; opacity: 0.7; margin-bottom: 1rem; } + .section { margin: 1rem 0; border: 1px solid var(--accent); border-radius: 8px; overflow: hidden; } + .section-header { padding: 0.75rem 1rem; cursor: pointer; display: flex; justify-content: space-between; align-items: center; background: var(--code-bg); } + .section-header::before { content: '▼'; margin-right: 0.5rem; transition: transform 0.2s; } + .section.collapsed .section-header::before { transform: rotate(-90deg); } + .section-content { padding: 1rem; display: block; overflow-x: auto; background: var(--code-bg); } + .section.collapsed .section-content { display: none; } + pre { margin: 0; white-space: pre-wrap; word-break: break-word; font-size: 0.875rem; font-family: ui-monospace, monospace; } + .copy-btn { background: var(--accent); color: white; border: none; padding: 0.25rem 0.5rem; border-radius: 4px; cursor: pointer; font-size: 0.75rem; } + .copy-btn:hover { opacity: 0.8; } + .footer { font-size: 0.75rem; opacity: 0.5; margin-top: 2rem; text-align: center; } + </style> +</head> +<body> + <h1>{{tool_name}}</h1> + <p class="meta">{{created_at}} · {{duration_ms}}ms</p> + + <div class="section" id="input-section"> + <div class="section-header"> + Input + <button class="copy-btn" data-target="input-content">Copy</button> + </div> + <div class="section-content" id="input-content"><pre>{{input_json}}</pre></div> + </div> + + <div class="section" id="output-section"> + <div class="section-header"> + Output + <button class="copy-btn" data-target="output-content">Copy</button> + </div> + <div class="section-content" id="output-content"><pre>{{output_json}}</pre></div> + </div> + + <p class="footer">Trace ID: {{trace_id}}</p> + + <script> + document.querySelectorAll('.section-header').forEach(header => { + header.addEventListener('click', (e) => { + if (e.target.classList.contains('copy-btn')) return; + header.parentElement.classList.toggle('collapsed'); + }); + }); + + document.querySelectorAll('.copy-btn').forEach(btn => { + btn.addEventListener('click', () => { + const targetId = btn.getAttribute('data-target'); + const text = document.getElementById(targetId).querySelector('pre').textContent; + navigator.clipboard.writeText(text).then(() => { + const orig = btn.textContent; + btn.textContent = 'Copied!'; + setTimeout(() => btn.textContent = orig, 1500); + }); + }); + }); + </script> +</body> +</html> |
