Skip to main content
Glama
viewer.html28.8 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>MCP Browser Use - Task Monitor</title> <style> :root { --bg-primary: #ffffff; --bg-secondary: #f6f8fa; --bg-tertiary: #f0f2f5; --text-primary: #24292f; --text-secondary: #57606a; --text-tertiary: #6e7781; --border-color: #d0d7de; --status-running: #0969da; --status-completed: #1a7f37; --status-failed: #cf222e; --status-pending: #bf8700; --status-cancelled: #6e7781; --health-online: #1a7f37; --health-offline: #cf222e; --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1); } [data-theme="dark"] { --bg-primary: #0d1117; --bg-secondary: #161b22; --bg-tertiary: #1c2128; --text-primary: #e6edf3; --text-secondary: #9198a1; --text-tertiary: #7d8590; --border-color: #30363d; --status-running: #4493f8; --status-completed: #3fb950; --status-failed: #f85149; --status-pending: #d29922; --status-cancelled: #8b949e; --health-online: #3fb950; --health-offline: #f85149; --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4); } * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background: var(--bg-secondary); color: var(--text-primary); line-height: 1.5; } .container { max-width: 1200px; margin: 0 auto; padding: 20px; } /* Health Bar */ .health-bar { background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 6px; padding: 16px 20px; margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; box-shadow: var(--shadow-sm); } .health-status { display: flex; align-items: center; gap: 12px; } .status-dot { width: 12px; height: 12px; border-radius: 50%; animation: pulse 2s infinite; } .status-dot.online { background: var(--health-online); } .status-dot.offline { background: var(--health-offline); } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } .health-info { display: flex; gap: 20px; font-size: 14px; color: var(--text-secondary); } .health-info span { display: flex; align-items: center; gap: 6px; } /* Theme Toggle */ .theme-toggle { background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 6px; padding: 6px 12px; cursor: pointer; font-size: 14px; color: var(--text-primary); transition: all 0.2s; } .theme-toggle:hover { background: var(--bg-secondary); } /* Task List */ .task-list { background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 6px; overflow: hidden; box-shadow: var(--shadow-sm); } .task-list-header { padding: 16px 20px; border-bottom: 1px solid var(--border-color); font-weight: 600; font-size: 16px; } .task-item { padding: 16px 20px; border-bottom: 1px solid var(--border-color); cursor: pointer; transition: background 0.2s; } .task-item:hover { background: var(--bg-secondary); } .task-item:last-child { border-bottom: none; } .task-item.highlight { animation: highlight 1s; } @keyframes highlight { 0%, 100% { background: var(--bg-primary); } 50% { background: var(--bg-tertiary); } } .task-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; } .task-main { display: flex; align-items: center; gap: 12px; flex: 1; min-width: 0; } .task-id { font-family: "SF Mono", Monaco, "Cascadia Code", monospace; font-size: 13px; color: var(--text-tertiary); flex-shrink: 0; } .task-tool { font-weight: 500; color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .task-meta { display: flex; align-items: center; gap: 12px; flex-shrink: 0; } .status-badge { padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: 500; color: white; text-transform: capitalize; } .status-badge.running { background: var(--status-running); } .status-badge.completed { background: var(--status-completed); } .status-badge.failed { background: var(--status-failed); } .status-badge.pending { background: var(--status-pending); } .status-badge.cancelled { background: var(--status-cancelled); } .task-progress { font-size: 13px; color: var(--text-secondary); font-family: "SF Mono", Monaco, monospace; } .task-duration { font-size: 13px; color: var(--text-tertiary); } /* Task Details */ .task-details { background: var(--bg-tertiary); padding: 16px 20px; margin-top: 12px; border-radius: 6px; font-size: 14px; } .detail-row { margin-bottom: 12px; } .detail-row:last-child { margin-bottom: 0; } .detail-label { font-weight: 600; color: var(--text-secondary); margin-bottom: 4px; } .detail-value { color: var(--text-primary); word-break: break-word; } .detail-value.mono { font-family: "SF Mono", Monaco, monospace; font-size: 13px; background: var(--bg-secondary); padding: 8px 12px; border-radius: 4px; overflow-x: auto; } .progress-bar { background: var(--bg-secondary); border-radius: 4px; height: 8px; overflow: hidden; margin-top: 4px; } .progress-fill { background: var(--status-running); height: 100%; transition: width 0.3s; } .timestamps { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; } /* Empty State */ .empty-state { text-align: center; padding: 60px 20px; color: var(--text-secondary); } .empty-state svg { width: 64px; height: 64px; margin-bottom: 16px; opacity: 0.5; } /* Error State */ .error-state { background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 6px; padding: 40px 20px; text-align: center; color: var(--text-secondary); } .error-state .status-dot { margin: 0 auto 16px; } /* Responsive */ @media (max-width: 768px) { .container { padding: 12px; } .health-bar { flex-direction: column; align-items: flex-start; gap: 12px; } .health-info { flex-direction: column; gap: 8px; } .task-header { flex-direction: column; align-items: flex-start; } .task-meta { width: 100%; justify-content: space-between; } } /* Loading spinner */ .spinner { display: inline-block; width: 14px; height: 14px; border: 2px solid var(--text-tertiary); border-top-color: transparent; border-radius: 50%; animation: spin 0.8s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } </style> </head> <body> <div class="container"> <div class="health-bar"> <div class="health-status"> <div class="status-dot" id="statusDot"></div> <div> <strong id="statusText">Connecting...</strong> </div> </div> <div class="health-info"> <span>Uptime: <strong id="uptime">-</strong></span> <span>Memory: <strong id="memory">-</strong></span> <span>Running: <strong id="runningCount">-</strong></span> <button class="theme-toggle" id="themeToggle">🌙 Dark</button> </div> </div> <div class="task-list" id="taskListContainer"> <div class="task-list-header">Recent Tasks</div> <div id="taskList"> <div class="empty-state"> <div class="spinner"></div> <p>Loading tasks...</p> </div> </div> </div> </div> <script> const API_BASE = 'http://localhost:8383/api'; const REFRESH_INTERVAL = 2000; // 2 seconds let lastTaskIds = new Set(); let expandedTaskId = null; let refreshTimer = null; // HTML escaping utility to prevent XSS function escapeHtml(unsafe) { if (unsafe === null || unsafe === undefined) return ''; return String(unsafe) .replace(/&/g, "&amp;") .replace(/</g, "&lt;") .replace(/>/g, "&gt;") .replace(/"/g, "&quot;") .replace(/'/g, "&#039;"); } // Theme management function initTheme() { const saved = localStorage.getItem('theme'); const theme = saved || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); document.documentElement.setAttribute('data-theme', theme); updateThemeButton(theme); } function toggleTheme() { const current = document.documentElement.getAttribute('data-theme'); const next = current === 'dark' ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', next); localStorage.setItem('theme', next); updateThemeButton(next); } function updateThemeButton(theme) { const btn = document.getElementById('themeToggle'); btn.textContent = theme === 'dark' ? '☀️ Light' : '🌙 Dark'; } // API calls async function fetchHealth() { const response = await fetch(`${API_BASE}/health`); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } async function fetchTasks(limit = 20, status = null) { const params = new URLSearchParams({ limit: limit.toString() }); if (status) params.append('status', status); const response = await fetch(`${API_BASE}/tasks?${params}`); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } async function fetchTaskDetails(taskId) { const response = await fetch(`${API_BASE}/tasks/${taskId}`); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } // Update health status function updateHealth(health) { const dot = document.getElementById('statusDot'); const text = document.getElementById('statusText'); const uptime = document.getElementById('uptime'); const memory = document.getElementById('memory'); const running = document.getElementById('runningCount'); dot.className = 'status-dot online'; text.textContent = 'Daemon Online'; const uptimeMin = Math.floor(health.uptime_seconds / 60); uptime.textContent = uptimeMin < 1 ? '< 1m' : `${uptimeMin}m`; memory.textContent = `${health.memory_mb} MB`; running.textContent = health.running_tasks; } function showOffline(error) { const dot = document.getElementById('statusDot'); const text = document.getElementById('statusText'); dot.className = 'status-dot offline'; text.textContent = 'Daemon Offline'; document.getElementById('uptime').textContent = '-'; document.getElementById('memory').textContent = '-'; document.getElementById('runningCount').textContent = '-'; } // Format duration function formatDuration(seconds) { if (!seconds) return '-'; if (seconds < 1) return '< 1s'; if (seconds < 60) return `${Math.round(seconds)}s`; const min = Math.floor(seconds / 60); const sec = Math.round(seconds % 60); return `${min}m ${sec}s`; } // Format timestamp function formatTime(isoString) { if (!isoString) return '-'; const date = new Date(isoString); return date.toLocaleTimeString(); } // Render task list function renderTasks(data) { const container = document.getElementById('taskList'); const tasks = data.tasks || []; // Clear existing content container.innerHTML = ''; if (tasks.length === 0) { const emptyState = document.createElement('div'); emptyState.className = 'empty-state'; const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('viewBox', '0 0 24 24'); svg.setAttribute('fill', 'none'); svg.setAttribute('stroke', 'currentColor'); svg.setAttribute('stroke-width', '2'); const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); circle.setAttribute('cx', '12'); circle.setAttribute('cy', '12'); circle.setAttribute('r', '10'); const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('d', 'M12 6v6l4 2'); svg.appendChild(circle); svg.appendChild(path); const p = document.createElement('p'); p.textContent = 'No tasks yet'; emptyState.appendChild(svg); emptyState.appendChild(p); container.appendChild(emptyState); return; } // Detect new tasks for highlighting const currentIds = new Set(tasks.map(t => t.task_id)); const newIds = new Set([...currentIds].filter(id => !lastTaskIds.has(id))); lastTaskIds = currentIds; tasks.forEach(task => { const isNew = newIds.has(task.task_id); const isExpanded = expandedTaskId === task.task_id; const taskItem = document.createElement('div'); taskItem.className = `task-item ${isNew ? 'highlight' : ''}`; taskItem.setAttribute('data-task-id', task.task_id); taskItem.onclick = () => toggleTaskDetails(task.task_id); const taskHeader = document.createElement('div'); taskHeader.className = 'task-header'; const taskMain = document.createElement('div'); taskMain.className = 'task-main'; const taskId = document.createElement('span'); taskId.className = 'task-id'; taskId.textContent = task.task_id; const taskTool = document.createElement('span'); taskTool.className = 'task-tool'; taskTool.textContent = task.tool; taskMain.appendChild(taskId); taskMain.appendChild(taskTool); const taskMeta = document.createElement('div'); taskMeta.className = 'task-meta'; const statusBadge = document.createElement('span'); statusBadge.className = `status-badge ${task.status}`; statusBadge.textContent = task.status; const taskProgress = document.createElement('span'); taskProgress.className = 'task-progress'; taskProgress.textContent = task.progress; const taskDuration = document.createElement('span'); taskDuration.className = 'task-duration'; taskDuration.textContent = formatDuration(task.duration_sec); taskMeta.appendChild(statusBadge); taskMeta.appendChild(taskProgress); taskMeta.appendChild(taskDuration); taskHeader.appendChild(taskMain); taskHeader.appendChild(taskMeta); taskItem.appendChild(taskHeader); if (isExpanded) { const taskDetails = document.createElement('div'); taskDetails.className = 'task-details'; taskDetails.id = `details-${task.task_id}`; const spinner = document.createElement('div'); spinner.className = 'spinner'; taskDetails.appendChild(spinner); taskDetails.appendChild(document.createTextNode(' Loading...')); taskItem.appendChild(taskDetails); } container.appendChild(taskItem); }); // If a task was expanded, reload its details if (expandedTaskId) { loadTaskDetails(expandedTaskId); } } // Toggle task details async function toggleTaskDetails(taskId) { if (expandedTaskId === taskId) { expandedTaskId = null; refresh(); } else { expandedTaskId = taskId; refresh(); } } // Helper to create detail row function createDetailRow(label, value, className = '') { const row = document.createElement('div'); row.className = 'detail-row'; const labelDiv = document.createElement('div'); labelDiv.className = 'detail-label'; labelDiv.textContent = label; const valueDiv = document.createElement('div'); valueDiv.className = `detail-value ${className}`; valueDiv.textContent = value; row.appendChild(labelDiv); row.appendChild(valueDiv); return row; } // Load full task details async function loadTaskDetails(taskId) { const container = document.getElementById(`details-${taskId}`); if (!container) return; try { const task = await fetchTaskDetails(taskId); const progressPercent = task.progress.percent || 0; // Clear existing content container.innerHTML = ''; // Task ID container.appendChild(createDetailRow('Task ID', task.task_id, 'mono')); // Progress section const progressRow = document.createElement('div'); progressRow.className = 'detail-row'; const progressLabel = document.createElement('div'); progressLabel.className = 'detail-label'; progressLabel.textContent = 'Progress'; const progressMessage = document.createElement('div'); progressMessage.className = 'detail-value'; progressMessage.textContent = task.progress.message || 'No message'; const progressBar = document.createElement('div'); progressBar.className = 'progress-bar'; const progressFill = document.createElement('div'); progressFill.className = 'progress-fill'; progressFill.style.width = `${progressPercent}%`; progressBar.appendChild(progressFill); const progressStats = document.createElement('div'); progressStats.className = 'detail-value'; progressStats.style.fontSize = '12px'; progressStats.style.color = 'var(--text-tertiary)'; progressStats.style.marginTop = '4px'; progressStats.textContent = `${progressPercent}% (${task.progress.current}/${task.progress.total})`; progressRow.appendChild(progressLabel); progressRow.appendChild(progressMessage); progressRow.appendChild(progressBar); progressRow.appendChild(progressStats); container.appendChild(progressRow); // Timestamps section const timestampsRow = document.createElement('div'); timestampsRow.className = 'detail-row'; const timestampsLabel = document.createElement('div'); timestampsLabel.className = 'detail-label'; timestampsLabel.textContent = 'Timestamps'; const timestampsGrid = document.createElement('div'); timestampsGrid.className = 'timestamps'; ['created', 'started', 'completed'].forEach(type => { const timestampItem = document.createElement('div'); const timestampLabel = document.createElement('div'); timestampLabel.style.fontSize = '12px'; timestampLabel.style.color = 'var(--text-tertiary)'; timestampLabel.textContent = type.charAt(0).toUpperCase() + type.slice(1); const timestampValue = document.createElement('div'); timestampValue.className = 'detail-value'; timestampValue.textContent = formatTime(task.timestamps[type]); timestampItem.appendChild(timestampLabel); timestampItem.appendChild(timestampValue); timestampsGrid.appendChild(timestampItem); }); timestampsRow.appendChild(timestampsLabel); timestampsRow.appendChild(timestampsGrid); container.appendChild(timestampsRow); // Input parameters (if present) if (task.input) { container.appendChild(createDetailRow('Input Parameters', JSON.stringify(task.input, null, 2), 'mono')); } // Result (if present) if (task.result) { container.appendChild(createDetailRow('Result (preview)', task.result, 'mono')); } // Error (if present) if (task.error) { const errorRow = createDetailRow('Error', task.error, 'mono'); errorRow.querySelector('.detail-value').style.color = 'var(--status-failed)'; container.appendChild(errorRow); } // Stage (if present) if (task.stage) { container.appendChild(createDetailRow('Stage', task.stage)); } } catch (error) { container.innerHTML = ''; const errorRow = document.createElement('div'); errorRow.className = 'detail-row'; const errorValue = document.createElement('div'); errorValue.className = 'detail-value'; errorValue.style.color = 'var(--status-failed)'; errorValue.textContent = `Failed to load details: ${error.message}`; errorRow.appendChild(errorValue); container.appendChild(errorRow); } } // Main refresh function async function refresh() { try { const [health, tasks] = await Promise.all([ fetchHealth(), fetchTasks() ]); updateHealth(health); renderTasks(tasks); } catch (error) { console.error('Refresh error:', error); showOffline(error); // Show error state in task list if this is the first load if (lastTaskIds.size === 0) { const taskList = document.getElementById('taskList'); taskList.innerHTML = ''; const errorState = document.createElement('div'); errorState.className = 'error-state'; const statusDot = document.createElement('div'); statusDot.className = 'status-dot offline'; const title = document.createElement('p'); const strong = document.createElement('strong'); strong.textContent = 'Connection failed'; title.appendChild(strong); const instructions = document.createElement('p'); instructions.style.fontSize = '14px'; instructions.style.marginTop = '8px'; instructions.textContent = 'Make sure the daemon is running:'; const br = document.createElement('br'); instructions.appendChild(br); const code = document.createElement('code'); code.style.background = 'var(--bg-tertiary)'; code.style.padding = '4px 8px'; code.style.borderRadius = '4px'; code.style.marginTop = '8px'; code.style.display = 'inline-block'; code.textContent = 'mcp-server-browser-use server'; instructions.appendChild(code); errorState.appendChild(statusDot); errorState.appendChild(title); errorState.appendChild(instructions); taskList.appendChild(errorState); } } } // Auto-refresh function startAutoRefresh() { refresh(); refreshTimer = setInterval(refresh, REFRESH_INTERVAL); } function stopAutoRefresh() { if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; } } // Initialize document.getElementById('themeToggle').addEventListener('click', toggleTheme); initTheme(); startAutoRefresh(); // Cleanup on page unload window.addEventListener('beforeunload', stopAutoRefresh); // Make toggleTaskDetails available globally window.toggleTaskDetails = toggleTaskDetails; </script> </body> </html>

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Saik0s/mcp-browser-use'

If you have feedback or need assistance with the MCP directory API, please join our Discord server