UNPKG

rabbitize

Version:

Transform visual UI interactions into automated processes

476 lines (417 loc) 22.7 kB
<!DOCTYPE html> <html> <head> <title>{{CLIENT_ID}} - RABBITIZE CLIENT</title> <link rel="stylesheet" href="/resources/streaming/themes/theme-variables.css"> <link rel="stylesheet" href="/resources/streaming/themes/theme-styles.css"> <link rel="stylesheet" href="/resources/streaming/cyberpunk.css"> <link rel="stylesheet" href="/resources/streaming/themes/theme-switcher.css"> <link rel="icon" type="image/x-icon" href="/resources/streaming/favicon.png"> </head> <body> <a href="/streaming" class="back-link">[ ← BACK TO ALL CLIENTS ]</a> <div class="single-client-container"> <div class="header"> <h1 class="glitch" data-text="CLIENT /// {{CLIENT_ID}}">CLIENT /// {{CLIENT_ID}}</h1> <div class="subtitle">ALL TESTS AND SESSIONS</div> </div> <div id="tests-container"> <!-- Tests will be loaded here --> </div> </div> <!-- Step detail overlay --> <div id="step-overlay" class="step-overlay"> <span class="step-overlay-close" onclick="closeStepOverlay()">&times;</span> <div class="step-overlay-content" id="step-overlay-content"> <!-- Content will be dynamically loaded here --> </div> </div> <script> const clientId = '{{CLIENT_ID}}'; // Copy necessary functions from dashboard function renderSessionCard(session, statusClass, statusText, uptime, isHistorical) { const sessionKey = `${session.clientId}/${session.testId}/${session.sessionId}`; return ` <div class="session-card ${isHistorical ? 'historical' : ''}" data-session-id="${session.sessionId}" data-session-key="${sessionKey}"> <div class="timestamp">${new Date(session.startTime).toISOString()}</div> <div class="session-info"> <div class="session-cover"> <a href="/single-session/${session.clientId}/${session.testId}/${session.sessionId}"> <img src="${session.status === 'finished' || session.phase === 'completed' ? `/rabbitize-runs/${session.clientId}/${session.testId}/${session.sessionId}/video/cover.gif` : '/resources/streaming/images/running.gif'}" alt="Session preview" onerror="this.style.display='none'" loading="lazy"> </a> </div> ${session.initialUrl ? ` <div class="info-item url-item"> <div class="info-label">URL</div> <div class="info-value url-value" title="${session.initialUrl}">${session.initialUrl}</div> </div> ` : ''} <div class="info-item"> <div class="info-label">Status</div> <div class="info-value"><span class="${statusClass}"></span>${statusText}</div> </div> <div class="info-item"> <div class="info-label">Session ID</div> <div class="info-value"> <a href="/single-session/${session.clientId}/${session.testId}/${session.sessionId}" class="session-id-link">${session.sessionId}</a> </div> </div> <div class="info-item"> <div class="info-label">Steps</div> <div class="info-value command-count">${session.commandCount}</div> </div> <div class="info-item"> <div class="info-label">Phase</div> <div class="info-value phase">${session.phase || 'unknown'}</div> </div> <div class="info-item"> <div class="info-label">Uptime</div> <div class="info-value uptime">${uptime}</div> </div> ${session.isExternal ? ` <div class="info-item"> <div class="info-label">External</div> <div class="info-value"><span style="color: #f0f;">Port ${session.port || 'Unknown'}</span></div> </div> ` : ''} </div> <div class="zoom-preview-container" data-session-key="${sessionKey}"> <div class="zoom-preview-loading">Loading previews...</div> </div> <div class="timing-chart-container" data-session-key="${sessionKey}"> <!-- Timing chart will be loaded here --> </div> <div class="actions"> ${session.status === 'active' ? ` <a href="${session.isExternal && session.port ? `http://${window.location.hostname}:${session.port}` : ''}/stream/${session.clientId}/${session.testId}/${session.sessionId}" class="action-link" target="_blank"> [ DIRECT STREAM ] </a> <a href="/stream-viewer/${session.clientId}/${session.testId}/${session.sessionId}" class="action-link" target="_blank"> [ WEB VIEWER ] </a> ` : ` <a href="/stream-viewer/${session.clientId}/${session.testId}/${session.sessionId}" class="action-link" target="_blank"> [ WATCH VIDEO ] </a> `} </div> </div> `; } async function loadClientTests() { try { const response = await fetch('/api/sessions'); const sessions = await response.json(); // Filter sessions for this client const clientSessions = sessions.filter(s => s.clientId === clientId); if (clientSessions.length === 0) { document.getElementById('tests-container').innerHTML = '<div class="no-sessions">No tests found for this client</div>'; return; } // Group by test ID const testGroups = {}; clientSessions.forEach(session => { if (!testGroups[session.testId]) { testGroups[session.testId] = []; } testGroups[session.testId].push(session); }); // Render tests let html = ''; Object.entries(testGroups).forEach(([testId, sessions]) => { const testGroupId = `test-${testId}`; const activeTestSessions = sessions.filter(s => s.status === 'active').length; html += ` <div class="test-group" id="${testGroupId}"> <div class="test-header"> <a href="/single-test/${clientId}/${testId}" class="test-header-link"> <h3>TEST: ${testId}</h3> </a> <div class="client-stats"> <span>${sessions.length} run${sessions.length !== 1 ? 's' : ''}</span> ${activeTestSessions > 0 ? `<span style="color: #0f0;">${activeTestSessions} active</span>` : ''} </div> </div> <div class="test-content"> `; // Render sessions for this test sessions.forEach(session => { const statusClass = session.status === 'active' ? 'status-indicator' : 'status-indicator-finished'; const statusText = session.status.toUpperCase(); const uptime = session.status === 'active' ? Math.floor((Date.now() - session.startTime) / 1000) + 's' : Math.floor((session.duration || 0) / 1000) + 's'; const isHistorical = session.phase === 'legacy' || session.phase === 'unknown'; html += renderSessionCard(session, statusClass, statusText, uptime, isHistorical); }); html += ` </div> </div> `; }); document.getElementById('tests-container').innerHTML = html; // Load zoom previews and timing data loadSessionDetails(); } catch (error) { console.error('Failed to load client tests:', error); document.getElementById('tests-container').innerHTML = '<div class="no-sessions">Failed to load tests</div>'; } } // Load tests initially loadClientTests(); // Update only dynamic content every second setInterval(async () => { try { const response = await fetch('/api/sessions'); const sessions = await response.json(); // Filter sessions for this client const clientSessions = sessions.filter(s => s.clientId === clientId); // Update only the dynamic parts of existing session cards clientSessions.forEach(session => { const sessionKey = `${session.clientId}/${session.testId}/${session.sessionId}`; const card = document.querySelector(`.session-card[data-session-key="${sessionKey}"]`); if (card) { // Update status const statusElement = card.querySelector('.info-value .status-indicator, .info-value .status-indicator-finished'); if (statusElement) { const newClass = session.status === 'active' ? 'status-indicator' : 'status-indicator-finished'; statusElement.className = newClass; statusElement.nextSibling.textContent = session.status.toUpperCase(); } // Update command count const commandCount = card.querySelector('.command-count'); if (commandCount) { commandCount.textContent = session.commandCount; } // Update phase const phase = card.querySelector('.phase'); if (phase) { phase.textContent = session.phase || 'unknown'; } // Update uptime const uptime = card.querySelector('.uptime'); if (uptime) { const uptimeValue = session.status === 'active' ? Math.floor((Date.now() - session.startTime) / 1000) + 's' : Math.floor((session.duration || 0) / 1000) + 's'; uptime.textContent = uptimeValue; } } }); // Also update test group stats const testGroups = {}; clientSessions.forEach(session => { if (!testGroups[session.testId]) { testGroups[session.testId] = { total: 0, active: 0 }; } testGroups[session.testId].total++; if (session.status === 'active') { testGroups[session.testId].active++; } }); Object.entries(testGroups).forEach(([testId, stats]) => { const testHeader = document.querySelector(`#test-${testId} .client-stats`); if (testHeader) { testHeader.innerHTML = ` <span>${stats.total} run${stats.total !== 1 ? 's' : ''}</span> ${stats.active > 0 ? `<span style="color: #0f0;">${stats.active} active</span>` : ''} `; } }); } catch (error) { console.error('Failed to update session status:', error); } }, 1000); // Check for new tests/sessions less frequently setInterval(loadClientTests, 10000); // Function to render timing chart function renderTimingChart(timingData, totalDuration, clientId, testId, sessionId) { if (!timingData || timingData.length === 0) { return '<div class="timing-chart-empty">No timing data available</div>'; } let html = '<div class="timing-chart">'; timingData.forEach((item, index) => { const startPercent = (item.relativeStart / totalDuration) * 100; const widthPercent = (item.duration / totalDuration) * 100; if (item.gapBefore > 0) { const gapStartPercent = ((item.relativeStart - item.gapBefore) / totalDuration) * 100; const gapWidthPercent = (item.gapBefore / totalDuration) * 100; html += `<div class="timing-gap" style="left: ${gapStartPercent}%; width: ${gapWidthPercent}%;"></div>`; } const barLabel = widthPercent > 5 ? `${index}` : ''; html += ` <div class="timing-bar" data-command="${item.command}" data-index="${index}" style="left: ${startPercent}%; width: ${widthPercent}%;" title="${item.command} - ${item.duration}ms" onclick="showStepDetails('${clientId}', '${testId}', '${sessionId}', ${index})"> ${barLabel} <div class="timing-tooltip"> Step ${index}: ${item.command}<br> Duration: ${(item.duration / 1000).toFixed(2)}s </div> </div> `; }); html += '</div>'; const totalSeconds = (totalDuration / 1000).toFixed(2); const avgDuration = (timingData.reduce((sum, item) => sum + item.duration, 0) / timingData.length / 1000).toFixed(2); html += ` <div class="timing-stats"> <div class="timing-stat"> <span class="timing-stat-label">Total:</span> <span>${totalSeconds}s</span> </div> <div class="timing-stat"> <span class="timing-stat-label">Steps:</span> <span>${timingData.length}</span> </div> <div class="timing-stat"> <span class="timing-stat-label">Avg:</span> <span>${avgDuration}s</span> </div> </div> `; return html; } // Function to lazy load session details async function loadSessionDetails() { const containers = document.querySelectorAll('.session-card'); for (const container of containers) { const sessionKey = container.dataset.sessionKey; const [clientId, testId, sessionId] = sessionKey.split('/'); try { const response = await fetch(`/api/session/${clientId}/${testId}/${sessionId}`); const details = await response.json(); // Update zoom previews const zoomContainer = container.querySelector('.zoom-preview-container'); if (zoomContainer && details.zoomImages && details.zoomImages.length > 0) { zoomContainer.innerHTML = ` <div class="zoom-preview-grid"> ${details.zoomImages.map(img => ` <img class="zoom-thumb" src="${img.url}" alt="Step ${img.index}" title="Step ${img.index}" data-index="${img.index}" loading="lazy" onclick="showStepDetails('${clientId}', '${testId}', '${sessionId}', ${img.index})"> `).join('')} </div> `; } else if (zoomContainer) { zoomContainer.innerHTML = '<div class="zoom-preview-empty">No preview images</div>'; } // Update timing chart const timingContainer = container.querySelector('.timing-chart-container'); if (timingContainer && details.timingData) { timingContainer.innerHTML = renderTimingChart(details.timingData, details.totalDuration, clientId, testId, sessionId); } } catch (error) { console.error('Failed to load session details:', error); } } } // Step overlay functions async function showStepDetails(clientId, testId, sessionId, stepIndex) { try { const response = await fetch(`/api/session/${clientId}/${testId}/${sessionId}/step/${stepIndex}`); const stepData = await response.json(); let content = ` <div class="step-screenshot-section"> `; if (stepData.screenshots.pre) { content += ` <div class="step-screenshot"> <img src="${stepData.screenshots.pre}" alt="Pre-command state"> <div class="step-screenshot-label">Before</div> </div> `; } if (stepData.screenshots.post) { content += ` <div class="step-screenshot"> <img src="${stepData.screenshots.post}" alt="Post-command state"> <div class="step-screenshot-label">After</div> </div> `; } content += `</div><div class="step-info-section">`; content += ` <div class="step-header"> <div class="step-title">Step ${stepIndex}</div> <div class="step-command-display">${JSON.stringify(stepData.command)}</div> </div> `; // Metrics if (stepData.timing || stepData.metrics) { content += '<div class="step-metrics">'; if (stepData.timing) { content += ` <div class="step-metric"> <div class="step-metric-label">Duration</div> <div class="step-metric-value">${(stepData.timing.duration / 1000).toFixed(2)}s</div> </div> `; } if (stepData.metrics?.pre?.cpu) { content += ` <div class="step-metric"> <div class="step-metric-label">CPU (Pre → Post)</div> <div class="step-metric-value">${stepData.metrics.pre.cpu}% → ${stepData.metrics.post.cpu}%</div> </div> `; } if (stepData.metrics?.pre?.memory) { content += ` <div class="step-metric"> <div class="step-metric-label">Memory (Pre → Post)</div> <div class="step-metric-value">${stepData.metrics.pre.memory}MB → ${stepData.metrics.post.memory}MB</div> </div> `; } content += '</div>'; } // DOM preview if (stepData.dom) { content += ` <div class="step-dom-preview">${stepData.dom.substring(0, 500)}${stepData.dom.length > 500 ? '...' : ''}</div> `; } content += '</div>'; if (stepData.videoClip) { content += ` <div class="step-video-section"> <video class="step-video" controls autoplay loop> <source src="${stepData.videoClip}" type="video/mp4"> </video> </div> `; } document.getElementById('step-overlay-content').innerHTML = content; document.getElementById('step-overlay').classList.add('active'); } catch (error) { console.error('Failed to load step details:', error); alert('Failed to load step details'); } } function closeStepOverlay() { document.getElementById('step-overlay').classList.remove('active'); } </script> <script src="/resources/streaming/dock.js"></script> <script src="/resources/streaming/themes/theme-switcher.js"></script> </body> </html>