Spaces:
Build error
Build error
| <html> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>Log Viewer</title> | |
| <script src="/api/v1/plugins/sandbox-client.js"></script> | |
| <style> | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { | |
| font-family: 'SF Mono', 'Menlo', 'Monaco', 'Consolas', monospace; | |
| font-size: 13px; | |
| background: #1e1e1e; | |
| color: #d4d4d4; | |
| height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| #controls { | |
| position: sticky; | |
| top: 0; | |
| background: #2d2d2d; | |
| padding: 8px 12px; | |
| border-bottom: 1px solid #444; | |
| display: flex; | |
| gap: 12px; | |
| align-items: center; | |
| flex-shrink: 0; | |
| z-index: 1; | |
| } | |
| #log-container { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 4px 0; | |
| } | |
| .log-entry { | |
| padding: 2px 12px; | |
| white-space: pre-wrap; | |
| word-break: break-all; | |
| border-bottom: 1px solid #2a2a2a; | |
| line-height: 1.4; | |
| } | |
| .log-entry:hover { background: #2a2a2a; } | |
| .level-DEBUG { color: #888; } | |
| .level-INFO { color: #d4d4d4; } | |
| .level-WARNING { color: #e5a84b; } | |
| .level-ERROR { color: #f44747; } | |
| .level-CRITICAL { color: #f44747; font-weight: bold; } | |
| .timestamp { color: #6a9955; } | |
| .logger-name { color: #569cd6; } | |
| .level-badge { | |
| display: inline-block; | |
| min-width: 70px; | |
| } | |
| button, select { | |
| background: #3c3c3c; | |
| color: #d4d4d4; | |
| border: 1px solid #555; | |
| padding: 4px 12px; | |
| cursor: pointer; | |
| border-radius: 3px; | |
| font-size: 12px; | |
| } | |
| button:hover { background: #4c4c4c; } | |
| label { | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| font-size: 12px; | |
| } | |
| #status { | |
| margin-left: auto; | |
| font-size: 11px; | |
| color: #888; | |
| } | |
| #count { | |
| font-size: 11px; | |
| color: #888; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="controls"> | |
| <button id="clearBtn">Clear</button> | |
| <label>Level: | |
| <select id="levelFilter"> | |
| <option value="DEBUG">DEBUG</option> | |
| <option value="INFO" selected>INFO</option> | |
| <option value="WARNING">WARNING</option> | |
| <option value="ERROR">ERROR</option> | |
| </select> | |
| </label> | |
| <label><input type="checkbox" id="autoScroll" checked> Auto-scroll</label> | |
| <label><input type="checkbox" id="pauseBtn"> Pause</label> | |
| <span id="status">Connecting...</span> | |
| <span id="count">0 entries</span> | |
| </div> | |
| <div id="log-container"></div> | |
| <script> | |
| const LEVELS = { DEBUG: 0, INFO: 1, WARNING: 2, ERROR: 3, CRITICAL: 4 }; | |
| const container = document.getElementById('log-container'); | |
| const statusEl = document.getElementById('status'); | |
| const countEl = document.getElementById('count'); | |
| const levelFilter = document.getElementById('levelFilter'); | |
| const autoScrollCb = document.getElementById('autoScroll'); | |
| const pauseCb = document.getElementById('pauseBtn'); | |
| let entryCount = 0; | |
| let paused = false; | |
| let pendingEntries = []; | |
| let currentSessionId = null; | |
| const MAX_ENTRIES = 5000; | |
| function escapeHtml(text) { | |
| const d = document.createElement('div'); | |
| d.textContent = text || ''; | |
| return d.innerHTML; | |
| } | |
| function appendEntry(data) { | |
| const minLevel = LEVELS[levelFilter.value] || 0; | |
| const entryLevel = LEVELS[data.level] || 0; | |
| if (entryLevel < minLevel) return; | |
| const div = document.createElement('div'); | |
| div.className = 'log-entry level-' + data.level; | |
| div.innerHTML = | |
| '<span class="timestamp">' + escapeHtml(data.timestamp) + '</span> ' | |
| + '<span class="level-badge level-' + data.level + '">[' + (data.level || '').padEnd(8) + ']</span> ' | |
| + '<span class="logger-name">' + escapeHtml(data.logger) + '</span> - ' | |
| + escapeHtml(data.message); | |
| container.appendChild(div); | |
| entryCount++; | |
| countEl.textContent = entryCount + ' entries'; | |
| while (container.children.length > MAX_ENTRIES) { | |
| container.removeChild(container.firstChild); | |
| } | |
| if (autoScrollCb.checked) { | |
| div.scrollIntoView({ block: 'end' }); | |
| } | |
| } | |
| function onLogEntry(data) { | |
| if (paused) { | |
| pendingEntries.push(data); | |
| return; | |
| } | |
| appendEntry(data); | |
| } | |
| async function loadRecentLogs(sessionId) { | |
| try { | |
| const response = await fetch('/api/plugins/log-viewer/recent?session_id=' + encodeURIComponent(sessionId)); | |
| if (!response.ok) { | |
| throw new Error('Failed to load recent logs: ' + response.status); | |
| } | |
| const data = await response.json(); | |
| if (data.entries && data.entries.length > 0) { | |
| data.entries.forEach(appendEntry); | |
| } | |
| } catch (e) { | |
| console.error('Error loading recent logs:', e); | |
| } | |
| } | |
| async function setLogLevel(level) { | |
| if (!currentSessionId) return; | |
| try { | |
| const response = await fetch( | |
| '/api/plugins/log-viewer/level?level=' + encodeURIComponent(level) + | |
| '&session_id=' + encodeURIComponent(currentSessionId), | |
| { method: 'POST' } | |
| ); | |
| if (!response.ok) { | |
| console.error('Failed to set log level:', response.status); | |
| } | |
| } catch (e) { | |
| console.error('Error setting log level:', e); | |
| } | |
| } | |
| function updateStatus(state, detail) { | |
| if (state === 'connected') { | |
| statusEl.textContent = 'Connected'; | |
| statusEl.style.color = '#6a9955'; | |
| } else if (state === 'reconnecting') { | |
| statusEl.textContent = 'Reconnecting...'; | |
| statusEl.style.color = '#e5a84b'; | |
| } else if (state === 'error') { | |
| statusEl.textContent = detail || 'Connection failed'; | |
| statusEl.style.color = '#f44747'; | |
| } | |
| } | |
| async function subscribeBackend() { | |
| try { | |
| const response = await fetch( | |
| '/api/plugins/log-viewer/subscribe?session_id=' + encodeURIComponent(currentSessionId), | |
| { method: 'POST' } | |
| ); | |
| if (!response.ok) { | |
| console.error('Backend subscribe failed:', response.status); | |
| return false; | |
| } | |
| return true; | |
| } catch (e) { | |
| console.error('Backend subscribe error:', e); | |
| return false; | |
| } | |
| } | |
| function onSSEConnected(_data) { | |
| // SSE stream was (re-)established — re-subscribe to the backend | |
| console.log('Log viewer: SSE reconnected, re-subscribing to backend'); | |
| subscribeBackend(); | |
| updateStatus('connected'); | |
| } | |
| async function init() { | |
| try { | |
| currentSessionId = new URLSearchParams(location.search).get('session_id'); | |
| // Load recent log entries first | |
| await loadRecentLogs(currentSessionId); | |
| // Verify sandbox is available before subscribing | |
| if (typeof window.sandbox === 'undefined' || typeof window.sandbox.subscribeSSE !== 'function') { | |
| throw new Error( | |
| 'Sandbox connection not available. ' | |
| + 'Please reopen the log viewer from the plugin menu. ' | |
| + '(If the problem persists, try reloading the application.)' | |
| ); | |
| } | |
| // Subscribe to SSE event forwarding FIRST so the listener is | |
| // ready before the backend starts sending events | |
| await sandbox.subscribeSSE('logEntry', onLogEntry); | |
| // Listen for SSE reconnection events to re-subscribe on the backend | |
| await sandbox.subscribeSSE('connected', onSSEConnected); | |
| // NOW subscribe on the backend (events start flowing, listener already active) | |
| await subscribeBackend(); | |
| updateStatus('connected'); | |
| // Periodic re-subscription as a safety net (idempotent) | |
| setInterval(() => { | |
| if (currentSessionId) { | |
| subscribeBackend(); | |
| } | |
| }, 60000); | |
| } catch (e) { | |
| console.error('Log viewer initialization failed:', e); | |
| updateStatus('error', 'Connection failed. Reopen log viewer from the plugin menu.'); | |
| } | |
| } | |
| document.getElementById('clearBtn').addEventListener('click', () => { | |
| container.innerHTML = ''; | |
| entryCount = 0; | |
| countEl.textContent = '0 entries'; | |
| }); | |
| pauseCb.addEventListener('change', (e) => { | |
| paused = e.target.checked; | |
| if (!paused) { | |
| pendingEntries.forEach(appendEntry); | |
| pendingEntries = []; | |
| } | |
| }); | |
| levelFilter.addEventListener('change', (e) => { | |
| setLogLevel(e.target.value); | |
| }); | |
| init(); | |
| </script> | |
| </body> | |
| </html> | |