Files
michaelschiemer/resources/js/modules/LiveComponentDevTools.js
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00

1796 lines
58 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* LiveComponent DevTools Overlay
*
* Entwickler-Werkzeuge für LiveComponents mit:
* - Component Tree Visualisierung
* - Action Log Viewer
* - Event Stream Monitor
* - Performance Profiling
* - State Inspector
* - Network Request Timeline
*
* @module LiveComponentDevTools
*/
import { Core } from '../core.js';
export class LiveComponentDevTools {
constructor() {
this.isOpen = false;
this.activeTab = 'components';
this.components = new Map();
this.actionLog = [];
this.eventLog = [];
this.networkLog = [];
this.performanceData = [];
this.domBadges = new Map(); // Track DOM badges for cleanup
this.badgesEnabled = true; // Badge visibility toggle
// Performance profiling state
this.isRecording = false;
this.performanceRecording = [];
this.componentRenderTimes = new Map();
this.actionExecutionTimes = new Map();
this.memorySnapshots = [];
this.overlay = null;
this.isEnabled = this.checkIfEnabled();
if (this.isEnabled) {
this.init();
}
}
/**
* Check if DevTools should be enabled
*/
checkIfEnabled() {
// Only in development mode
const isDev = document.documentElement.dataset.env === 'development';
// Or explicit activation via localStorage
const isExplicitlyEnabled = localStorage.getItem('livecomponent_devtools') === 'true';
return isDev || isExplicitlyEnabled;
}
/**
* Initialize DevTools
*/
init() {
this.createOverlay();
this.attachEventListeners();
this.registerGlobalShortcuts();
this.startMonitoring();
console.log('[LiveComponent DevTools] Initialized');
}
/**
* Create DevTools overlay DOM structure
*/
createOverlay() {
this.overlay = document.createElement('div');
this.overlay.id = 'livecomponent-devtools';
this.overlay.className = 'lc-devtools';
this.overlay.style.display = 'none';
this.overlay.innerHTML = `
<div class="lc-devtools__panel">
<div class="lc-devtools__header">
<div class="lc-devtools__title">
<span class="lc-devtools__icon">🛠️</span>
LiveComponent DevTools
</div>
<div class="lc-devtools__actions">
<button class="lc-devtools__btn lc-devtools__btn--small" data-action="toggle-badges" title="Toggle DOM badges">
⚡ Badges
</button>
<button class="lc-devtools__btn" data-action="minimize">_</button>
<button class="lc-devtools__btn" data-action="close">×</button>
</div>
</div>
<div class="lc-devtools__tabs">
<button class="lc-devtools__tab lc-devtools__tab--active" data-tab="components">
Components
</button>
<button class="lc-devtools__tab" data-tab="actions">
Actions
</button>
<button class="lc-devtools__tab" data-tab="events">
Events
</button>
<button class="lc-devtools__tab" data-tab="performance">
Performance
</button>
<button class="lc-devtools__tab" data-tab="network">
Network
</button>
</div>
<div class="lc-devtools__content">
<div class="lc-devtools__pane lc-devtools__pane--active" data-pane="components">
<div class="lc-devtools__toolbar">
<input
type="search"
class="lc-devtools__search"
placeholder="Filter components..."
data-filter="components"
/>
<button class="lc-devtools__btn lc-devtools__btn--small" data-action="refresh-components">
↻ Refresh
</button>
</div>
<div class="lc-devtools__tree" data-content="components"></div>
</div>
<div class="lc-devtools__pane" data-pane="actions">
<div class="lc-devtools__toolbar">
<input
type="search"
class="lc-devtools__search"
placeholder="Filter actions..."
data-filter="actions"
/>
<button class="lc-devtools__btn lc-devtools__btn--small" data-action="clear-actions">
Clear
</button>
</div>
<div class="lc-devtools__log" data-content="actions"></div>
</div>
<div class="lc-devtools__pane" data-pane="events">
<div class="lc-devtools__toolbar">
<input
type="search"
class="lc-devtools__search"
placeholder="Filter events..."
data-filter="events"
/>
<button class="lc-devtools__btn lc-devtools__btn--small" data-action="clear-events">
Clear
</button>
</div>
<div class="lc-devtools__log" data-content="events"></div>
</div>
<div class="lc-devtools__pane" data-pane="performance">
<div class="lc-devtools__toolbar">
<button class="lc-devtools__btn lc-devtools__btn--small" data-action="record-performance">
● Record
</button>
<button class="lc-devtools__btn lc-devtools__btn--small" data-action="clear-performance">
Clear
</button>
</div>
<div class="lc-devtools__metrics" data-content="performance"></div>
</div>
<div class="lc-devtools__pane" data-pane="network">
<div class="lc-devtools__toolbar">
<input
type="search"
class="lc-devtools__search"
placeholder="Filter requests..."
data-filter="network"
/>
<button class="lc-devtools__btn lc-devtools__btn--small" data-action="clear-network">
Clear
</button>
</div>
<div class="lc-devtools__timeline" data-content="network"></div>
</div>
</div>
</div>
`;
document.body.appendChild(this.overlay);
this.injectStyles();
}
/**
* Inject DevTools styles
*/
injectStyles() {
const styles = `
.lc-devtools {
position: fixed;
bottom: 20px;
right: 20px;
width: 800px;
height: 600px;
z-index: 999999;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 13px;
}
.lc-devtools__panel {
background: #1e1e1e;
border: 1px solid #3c3c3c;
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.lc-devtools__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: #252526;
border-bottom: 1px solid #3c3c3c;
color: #cccccc;
cursor: move;
}
.lc-devtools__title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
}
.lc-devtools__icon {
font-size: 16px;
}
.lc-devtools__actions {
display: flex;
gap: 8px;
}
.lc-devtools__btn {
background: transparent;
border: 1px solid #3c3c3c;
border-radius: 4px;
color: #cccccc;
cursor: pointer;
padding: 4px 12px;
font-size: 13px;
transition: all 0.2s;
}
.lc-devtools__btn:hover {
background: #3c3c3c;
border-color: #4c4c4c;
}
.lc-devtools__btn--small {
padding: 4px 8px;
font-size: 11px;
}
.lc-devtools__tabs {
display: flex;
background: #2d2d30;
border-bottom: 1px solid #3c3c3c;
overflow-x: auto;
}
.lc-devtools__tab {
background: transparent;
border: none;
border-bottom: 2px solid transparent;
color: #969696;
cursor: pointer;
padding: 10px 16px;
font-size: 12px;
transition: all 0.2s;
white-space: nowrap;
}
.lc-devtools__tab:hover {
color: #cccccc;
}
.lc-devtools__tab--active {
color: #ffffff;
border-bottom-color: #007acc;
}
.lc-devtools__content {
flex: 1;
overflow: hidden;
position: relative;
}
.lc-devtools__pane {
display: none;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.lc-devtools__pane--active {
display: flex;
}
.lc-devtools__toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: #252526;
border-bottom: 1px solid #3c3c3c;
}
.lc-devtools__search {
flex: 1;
background: #3c3c3c;
border: 1px solid #4c4c4c;
border-radius: 4px;
color: #cccccc;
padding: 6px 12px;
font-size: 12px;
}
.lc-devtools__search:focus {
outline: none;
border-color: #007acc;
}
.lc-devtools__tree,
.lc-devtools__log,
.lc-devtools__metrics,
.lc-devtools__timeline {
flex: 1;
overflow-y: auto;
padding: 16px;
color: #cccccc;
}
.lc-devtools__tree::-webkit-scrollbar,
.lc-devtools__log::-webkit-scrollbar,
.lc-devtools__metrics::-webkit-scrollbar,
.lc-devtools__timeline::-webkit-scrollbar {
width: 8px;
}
.lc-devtools__tree::-webkit-scrollbar-track,
.lc-devtools__log::-webkit-scrollbar-track,
.lc-devtools__metrics::-webkit-scrollbar-track,
.lc-devtools__timeline::-webkit-scrollbar-track {
background: #1e1e1e;
}
.lc-devtools__tree::-webkit-scrollbar-thumb,
.lc-devtools__log::-webkit-scrollbar-thumb,
.lc-devtools__metrics::-webkit-scrollbar-thumb,
.lc-devtools__timeline::-webkit-scrollbar-thumb {
background: #3c3c3c;
border-radius: 4px;
}
.lc-devtools--minimized {
height: auto;
}
.lc-devtools--minimized .lc-devtools__tabs,
.lc-devtools--minimized .lc-devtools__content {
display: none;
}
/* DOM Badge Styles */
.lc-dom-badge {
position: fixed;
background: rgba(30, 30, 30, 0.95);
border: 1px solid #007acc;
border-radius: 4px;
padding: 6px 10px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 11px;
color: #cccccc;
cursor: pointer;
transition: all 0.2s;
backdrop-filter: blur(4px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
pointer-events: auto;
}
.lc-dom-badge:hover {
background: rgba(30, 30, 30, 1);
border-color: #4ec9b0;
transform: scale(1.05);
}
.lc-dom-badge.lc-badge-active {
animation: badge-pulse 0.5s ease-out;
border-color: #4ec9b0;
}
@keyframes badge-pulse {
0% {
transform: scale(1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
50% {
transform: scale(1.1);
box-shadow: 0 4px 16px rgba(78, 201, 176, 0.5);
}
100% {
transform: scale(1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
}
.lc-badge-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
}
.lc-badge-icon {
font-size: 12px;
}
.lc-badge-name {
color: #569cd6;
font-weight: 600;
}
.lc-badge-id {
color: #858585;
font-size: 10px;
}
.lc-badge-activity {
display: flex;
gap: 8px;
font-size: 10px;
color: #4ec9b0;
}
.lc-badge-actions {
color: #dcdcaa;
}
`;
const styleSheet = document.createElement('style');
styleSheet.textContent = styles;
document.head.appendChild(styleSheet);
}
/**
* Attach event listeners
*/
attachEventListeners() {
// Header actions
this.overlay.querySelector('[data-action="toggle-badges"]')?.addEventListener('click', () => {
this.toggleBadges();
});
this.overlay.querySelector('[data-action="minimize"]')?.addEventListener('click', () => {
this.toggleMinimize();
});
this.overlay.querySelector('[data-action="close"]')?.addEventListener('click', () => {
this.close();
});
// Tab switching
this.overlay.querySelectorAll('[data-tab]').forEach(tab => {
tab.addEventListener('click', (e) => {
this.switchTab(e.target.dataset.tab);
});
});
// Toolbar actions
this.overlay.querySelector('[data-action="refresh-components"]')?.addEventListener('click', () => {
this.refreshComponents();
});
this.overlay.querySelector('[data-action="clear-actions"]')?.addEventListener('click', () => {
this.clearActionLog();
});
this.overlay.querySelector('[data-action="clear-events"]')?.addEventListener('click', () => {
this.clearEventLog();
});
this.overlay.querySelector('[data-action="record-performance"]')?.addEventListener('click', () => {
this.togglePerformanceRecording();
});
this.overlay.querySelector('[data-action="clear-performance"]')?.addEventListener('click', () => {
this.clearPerformanceData();
});
this.overlay.querySelector('[data-action="clear-network"]')?.addEventListener('click', () => {
this.clearNetworkLog();
});
// Make header draggable
this.makeDraggable();
}
/**
* Register global keyboard shortcuts
*/
registerGlobalShortcuts() {
document.addEventListener('keydown', (e) => {
// Ctrl/Cmd + Shift + D - Toggle DevTools
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'D') {
e.preventDefault();
this.toggle();
}
});
}
/**
* Start monitoring LiveComponents
*/
startMonitoring() {
// Intercept Core.emit for event logging
this.interceptCoreEmit();
// Monitor component initialization
this.monitorComponentInit();
// Monitor network requests
this.monitorNetworkRequests();
// Connect to LiveComponent manager
this.connectToLiveComponent();
// Initialize DOM badges
this.initializeDomBadges();
}
/**
* Connect to LiveComponent manager for action/event logging
*/
connectToLiveComponent() {
// Wait for LiveComponent to be available
const checkLiveComponent = () => {
if (window.LiveComponent) {
window.LiveComponent.enableDevTools(this);
console.log('[LiveComponent DevTools] Connected to LiveComponent manager');
} else {
// Retry after a short delay
setTimeout(checkLiveComponent, 100);
}
};
checkLiveComponent();
}
/**
* Toggle DevTools visibility
*/
toggle() {
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
/**
* Open DevTools
*/
open() {
this.overlay.style.display = 'block';
this.isOpen = true;
this.refreshComponents();
// Show badges when DevTools opens
if (this.badgesEnabled) {
this.updateDomBadges();
}
Core.emit('devtools:opened');
}
/**
* Close DevTools
*/
close() {
this.overlay.style.display = 'none';
this.isOpen = false;
Core.emit('devtools:closed');
}
/**
* Toggle minimize state
*/
toggleMinimize() {
this.overlay.classList.toggle('lc-devtools--minimized');
}
/**
* Switch active tab
*/
switchTab(tabName) {
this.activeTab = tabName;
// Update tab buttons
this.overlay.querySelectorAll('[data-tab]').forEach(tab => {
tab.classList.toggle('lc-devtools__tab--active', tab.dataset.tab === tabName);
});
// Update panes
this.overlay.querySelectorAll('[data-pane]').forEach(pane => {
pane.classList.toggle('lc-devtools__pane--active', pane.dataset.pane === tabName);
});
// Refresh content for active tab
this.refreshActiveTab();
}
/**
* Make overlay draggable
*/
makeDraggable() {
const header = this.overlay.querySelector('.lc-devtools__header');
let isDragging = false;
let currentX;
let currentY;
let initialX;
let initialY;
header.addEventListener('mousedown', (e) => {
if (e.target.closest('.lc-devtools__actions')) return;
isDragging = true;
initialX = e.clientX - this.overlay.offsetLeft;
initialY = e.clientY - this.overlay.offsetTop;
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
this.overlay.style.left = currentX + 'px';
this.overlay.style.top = currentY + 'px';
this.overlay.style.right = 'auto';
this.overlay.style.bottom = 'auto';
});
document.addEventListener('mouseup', () => {
isDragging = false;
});
}
/**
* Refresh active tab content
*/
refreshActiveTab() {
switch (this.activeTab) {
case 'components':
this.renderComponents();
break;
case 'actions':
this.renderActionLog();
break;
case 'events':
this.renderEventLog();
break;
case 'performance':
this.renderPerformanceData();
break;
case 'network':
this.renderNetworkLog();
break;
}
}
/**
* Refresh components tree
*/
refreshComponents() {
// Find all LiveComponent elements in DOM
this.components.clear();
document.querySelectorAll('[data-component-id]').forEach(element => {
const componentId = element.dataset.componentId;
const componentName = element.dataset.componentName || 'Unknown';
this.components.set(componentId, {
id: componentId,
name: componentName,
element: element,
state: this.extractComponentState(element),
props: this.extractComponentProps(element)
});
});
this.renderComponents();
}
/**
* Extract component state from element
*/
extractComponentState(element) {
// Try to get state from data attribute or custom property
return element.__liveComponentState || {};
}
/**
* Extract component props from element
*/
extractComponentProps(element) {
const props = {};
// Extract data-* attributes as props
for (const [key, value] of Object.entries(element.dataset)) {
if (key !== 'componentId' && key !== 'componentName') {
props[key] = value;
}
}
return props;
}
/**
* Render components tree
*/
renderComponents() {
const container = this.overlay.querySelector('[data-content="components"]');
if (!container) return;
if (this.components.size === 0) {
container.innerHTML = '<div style="color: #969696; text-align: center; padding: 40px;">No LiveComponents found</div>';
return;
}
let html = '<div class="lc-component-tree">';
for (const [id, component] of this.components) {
html += `
<div class="lc-component-item" data-component-id="${id}">
<div class="lc-component-header">
<span class="lc-component-name">${component.name}</span>
<span class="lc-component-id">#${id}</span>
</div>
<div class="lc-component-details" style="display: none;">
<div class="lc-component-section">
<strong>Props:</strong>
<pre>${JSON.stringify(component.props, null, 2)}</pre>
</div>
<div class="lc-component-section">
<strong>State:</strong>
<pre>${JSON.stringify(component.state, null, 2)}</pre>
</div>
</div>
</div>
`;
}
html += '</div>';
container.innerHTML = html;
// Add click handlers for component items
container.querySelectorAll('.lc-component-item').forEach(item => {
item.addEventListener('click', () => {
const details = item.querySelector('.lc-component-details');
details.style.display = details.style.display === 'none' ? 'block' : 'none';
});
});
}
/**
* Log action execution
* Called by LiveComponent manager
*/
logAction(componentId, actionName, params, startTime, endTime, success, error = null) {
const duration = endTime - startTime;
this.actionLog.unshift({
timestamp: Date.now(),
componentId,
actionName,
params,
duration,
success,
error
});
// Keep only last 100 actions
if (this.actionLog.length > 100) {
this.actionLog.pop();
}
// Track performance data if recording
if (this.isRecording) {
this.recordActionExecution(componentId, actionName, duration, startTime, endTime);
}
// Update DOM badge activity
this.updateBadgeActivity(componentId);
if (this.activeTab === 'actions') {
this.renderActionLog();
}
}
/**
* Render action log
*/
renderActionLog() {
const container = this.overlay.querySelector('[data-content="actions"]');
if (!container) return;
if (this.actionLog.length === 0) {
container.innerHTML = '<div style="color: #969696; text-align: center; padding: 40px;">No actions logged</div>';
return;
}
let html = '<div class="lc-action-log" style="font-family: monospace; font-size: 12px;">';
for (const action of this.actionLog) {
const time = new Date(action.timestamp).toLocaleTimeString();
const statusIcon = action.success ? '✓' : '✗';
const statusColor = action.success ? '#4ec9b0' : '#f48771';
const paramsPreview = JSON.stringify(action.params).substring(0, 50);
html += `
<div class="lc-log-entry" style="padding: 8px; border-bottom: 1px solid #3c3c3c; cursor: pointer;" data-action-index="${this.actionLog.indexOf(action)}">
<div style="display: flex; align-items: center; gap: 12px;">
<span style="color: ${statusColor};">${statusIcon}</span>
<span style="color: #858585;">${time}</span>
<span style="color: #569cd6;">${action.componentId}</span>
<span style="color: #dcdcaa;">${action.actionName}()</span>
<span style="color: #858585;">${action.duration.toFixed(2)}ms</span>
</div>
${action.error ? `
<div style="color: #f48771; padding-left: 24px; margin-top: 4px; font-size: 11px;">
Error: ${action.error}
</div>
` : ''}
${Object.keys(action.params).length > 0 ? `
<div style="color: #858585; padding-left: 24px; margin-top: 4px; font-size: 11px;">
${paramsPreview}${JSON.stringify(action.params).length > 50 ? '...' : ''}
</div>
` : ''}
</div>
`;
}
html += '</div>';
container.innerHTML = html;
// Add click handlers to show full details
container.querySelectorAll('.lc-log-entry').forEach(entry => {
entry.addEventListener('click', () => {
const index = parseInt(entry.dataset.actionIndex);
const action = this.actionLog[index];
console.log('[LiveComponent DevTools] Action details:', action);
});
});
}
/**
* Clear action log
*/
clearActionLog() {
this.actionLog = [];
this.renderActionLog();
}
/**
* Log event
* Called by LiveComponent manager or Core.emit interception
*/
logEvent(eventName, data, source = 'client') {
this.eventLog.unshift({
timestamp: Date.now(),
eventName,
data,
source
});
// Keep only last 100 events
if (this.eventLog.length > 100) {
this.eventLog.pop();
}
if (this.activeTab === 'events') {
this.renderEventLog();
}
}
/**
* Render event log
*/
renderEventLog() {
const container = this.overlay.querySelector('[data-content="events"]');
if (!container) return;
if (this.eventLog.length === 0) {
container.innerHTML = '<div style="color: #969696; text-align: center; padding: 40px;">No events logged</div>';
return;
}
let html = '<div class="lc-event-log" style="font-family: monospace; font-size: 12px;">';
for (const event of this.eventLog) {
const time = new Date(event.timestamp).toLocaleTimeString();
const sourceColor = event.source === 'server' ? '#ce9178' : '#4ec9b0';
const sourceIcon = event.source === 'server' ? '⬅' : '⬆';
html += `
<div class="lc-log-entry" style="padding: 8px; border-bottom: 1px solid #3c3c3c; cursor: pointer;" data-event-index="${this.eventLog.indexOf(event)}">
<div style="display: flex; align-items: center; gap: 12px;">
<span style="color: ${sourceColor};">${sourceIcon}</span>
<span style="color: #858585;">${time}</span>
<span style="color: #dcdcaa;">${event.eventName}</span>
<span style="color: #858585; font-size: 10px;">[${event.source}]</span>
</div>
${event.data && Object.keys(event.data).length > 0 ? `
<div style="color: #858585; padding-left: 24px; margin-top: 4px; font-size: 11px;">
${JSON.stringify(event.data).substring(0, 60)}${JSON.stringify(event.data).length > 60 ? '...' : ''}
</div>
` : ''}
</div>
`;
}
html += '</div>';
container.innerHTML = html;
// Add click handlers to show full details
container.querySelectorAll('.lc-log-entry').forEach(entry => {
entry.addEventListener('click', () => {
const index = parseInt(entry.dataset.eventIndex);
const event = this.eventLog[index];
console.log('[LiveComponent DevTools] Event details:', event);
});
});
}
/**
* Clear event log
*/
clearEventLog() {
this.eventLog = [];
this.renderEventLog();
}
/**
* Clear performance data
*/
clearPerformanceData() {
this.performanceData = [];
this.performanceRecording = [];
this.componentRenderTimes.clear();
this.actionExecutionTimes.clear();
this.memorySnapshots = [];
this.renderPerformanceData();
}
/**
* Toggle performance recording
*/
togglePerformanceRecording() {
this.isRecording = !this.isRecording;
const recordBtn = this.overlay?.querySelector('[data-action="record-performance"]');
if (recordBtn) {
if (this.isRecording) {
recordBtn.textContent = '■ Stop';
recordBtn.style.color = '#f48771';
this.startPerformanceRecording();
} else {
recordBtn.textContent = '● Record';
recordBtn.style.color = '#cccccc';
this.stopPerformanceRecording();
}
}
}
/**
* Start performance recording
*/
startPerformanceRecording() {
console.log('[LiveComponent DevTools] Performance recording started');
this.performanceRecording = [];
this.memorySnapshots = [];
// Take initial memory snapshot if available
if (performance.memory) {
this.takeMemorySnapshot();
}
// Start periodic memory snapshots (every 500ms)
this.memorySnapshotInterval = setInterval(() => {
if (this.isRecording && performance.memory) {
this.takeMemorySnapshot();
}
}, 500);
}
/**
* Stop performance recording
*/
stopPerformanceRecording() {
console.log('[LiveComponent DevTools] Performance recording stopped');
if (this.memorySnapshotInterval) {
clearInterval(this.memorySnapshotInterval);
}
// Take final memory snapshot
if (performance.memory) {
this.takeMemorySnapshot();
}
this.renderPerformanceData();
}
/**
* Record action execution for performance profiling
*/
recordActionExecution(componentId, actionName, duration, startTime, endTime) {
this.performanceRecording.push({
type: 'action',
componentId,
actionName,
duration,
startTime,
endTime,
timestamp: Date.now()
});
// Update action execution times map
const key = `${componentId}:${actionName}`;
if (!this.actionExecutionTimes.has(key)) {
this.actionExecutionTimes.set(key, []);
}
this.actionExecutionTimes.get(key).push(duration);
// Keep only last 100 recordings
if (this.performanceRecording.length > 100) {
this.performanceRecording.shift();
}
}
/**
* Record component render for performance profiling
*/
recordComponentRender(componentId, duration, startTime, endTime) {
if (!this.isRecording) return;
this.performanceRecording.push({
type: 'render',
componentId,
duration,
startTime,
endTime,
timestamp: Date.now()
});
// Update component render times map
if (!this.componentRenderTimes.has(componentId)) {
this.componentRenderTimes.set(componentId, []);
}
this.componentRenderTimes.get(componentId).push(duration);
// Keep only last 100 recordings
if (this.performanceRecording.length > 100) {
this.performanceRecording.shift();
}
}
/**
* Take memory snapshot
*/
takeMemorySnapshot() {
if (!performance.memory) return;
this.memorySnapshots.push({
timestamp: Date.now(),
usedJSHeapSize: performance.memory.usedJSHeapSize,
totalJSHeapSize: performance.memory.totalJSHeapSize,
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit
});
// Keep only last 100 snapshots
if (this.memorySnapshots.length > 100) {
this.memorySnapshots.shift();
}
}
/**
* Render performance data
*/
renderPerformanceData() {
const container = this.overlay.querySelector('[data-content="performance"]');
if (!container) return;
if (this.performanceRecording.length === 0 && this.memorySnapshots.length === 0) {
container.innerHTML = `
<div style="color: #969696; text-align: center; padding: 40px;">
<p>No performance data recorded</p>
<p style="font-size: 12px; margin-top: 12px;">Click "● Record" to start profiling</p>
</div>
`;
return;
}
let html = '<div class="lc-performance-view" style="padding: 0;">';
// Performance Summary
html += this.renderPerformanceSummary();
// Flamegraph
html += this.renderFlamegraph();
// Timeline
html += this.renderPerformanceTimeline();
// Memory Usage Chart
if (this.memorySnapshots.length > 0) {
html += this.renderMemoryChart();
}
html += '</div>';
container.innerHTML = html;
}
/**
* Render performance summary
*/
renderPerformanceSummary() {
const actionCount = this.performanceRecording.filter(r => r.type === 'action').length;
const renderCount = this.performanceRecording.filter(r => r.type === 'render').length;
const avgActionTime = actionCount > 0
? this.performanceRecording
.filter(r => r.type === 'action')
.reduce((sum, r) => sum + r.duration, 0) / actionCount
: 0;
const avgRenderTime = renderCount > 0
? this.performanceRecording
.filter(r => r.type === 'render')
.reduce((sum, r) => sum + r.duration, 0) / renderCount
: 0;
const totalDuration = this.performanceRecording.reduce((sum, r) => sum + r.duration, 0);
return `
<div class="lc-perf-summary" style="padding: 16px; border-bottom: 1px solid #3c3c3c; background: #252526;">
<h3 style="margin: 0 0 12px 0; color: #cccccc; font-size: 14px; font-weight: 600;">Performance Summary</h3>
<div style="display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; font-size: 12px;">
<div>
<div style="color: #858585; margin-bottom: 4px;">Total Events</div>
<div style="color: #4ec9b0; font-size: 18px; font-weight: 600;">${this.performanceRecording.length}</div>
</div>
<div>
<div style="color: #858585; margin-bottom: 4px;">Actions</div>
<div style="color: #dcdcaa; font-size: 18px; font-weight: 600;">${actionCount}</div>
</div>
<div>
<div style="color: #858585; margin-bottom: 4px;">Renders</div>
<div style="color: #569cd6; font-size: 18px; font-weight: 600;">${renderCount}</div>
</div>
<div>
<div style="color: #858585; margin-bottom: 4px;">Avg Action</div>
<div style="color: #dcdcaa; font-size: 18px; font-weight: 600;">${avgActionTime.toFixed(2)}ms</div>
</div>
<div>
<div style="color: #858585; margin-bottom: 4px;">Total Duration</div>
<div style="color: #f48771; font-size: 18px; font-weight: 600;">${totalDuration.toFixed(2)}ms</div>
</div>
</div>
</div>
`;
}
/**
* Render flamegraph
*/
renderFlamegraph() {
if (this.actionExecutionTimes.size === 0 && this.componentRenderTimes.size === 0) {
return '';
}
let html = `
<div class="lc-flamegraph" style="padding: 16px; border-bottom: 1px solid #3c3c3c;">
<h3 style="margin: 0 0 12px 0; color: #cccccc; font-size: 14px; font-weight: 600;">
Execution Breakdown (Flamegraph)
</h3>
`;
// Calculate total time for each component/action
const breakdown = [];
// Process action execution times
for (const [key, times] of this.actionExecutionTimes) {
const [componentId, actionName] = key.split(':');
const totalTime = times.reduce((sum, t) => sum + t, 0);
const avgTime = totalTime / times.length;
const count = times.length;
breakdown.push({
type: 'action',
componentId,
label: actionName,
totalTime,
avgTime,
count,
color: '#dcdcaa'
});
}
// Process component render times
for (const [componentId, times] of this.componentRenderTimes) {
const totalTime = times.reduce((sum, t) => sum + t, 0);
const avgTime = totalTime / times.length;
const count = times.length;
breakdown.push({
type: 'render',
componentId,
label: 'render',
totalTime,
avgTime,
count,
color: '#569cd6'
});
}
// Sort by total time descending
breakdown.sort((a, b) => b.totalTime - a.totalTime);
// Calculate max time for bar width
const maxTime = breakdown[0]?.totalTime || 1;
// Render bars
html += '<div style="font-family: monospace; font-size: 11px;">';
for (const item of breakdown.slice(0, 10)) { // Top 10
const widthPercent = (item.totalTime / maxTime) * 100;
const componentName = this.components.get(item.componentId)?.name || item.componentId.substring(0, 8);
html += `
<div style="margin-bottom: 8px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 4px; color: #858585;">
<span>
<span style="color: #569cd6;">${componentName}</span>
<span style="color: ${item.color};">.${item.label}()</span>
<span style="color: #858585;">×${item.count}</span>
</span>
<span>${item.totalTime.toFixed(2)}ms (avg: ${item.avgTime.toFixed(2)}ms)</span>
</div>
<div style="width: 100%; height: 20px; background: #3c3c3c; border-radius: 2px; overflow: hidden;">
<div style="width: ${widthPercent}%; height: 100%; background: ${item.color}; transition: width 0.3s;"></div>
</div>
</div>
`;
}
html += '</div>';
html += '</div>';
return html;
}
/**
* Render performance timeline
*/
renderPerformanceTimeline() {
if (this.performanceRecording.length === 0) {
return '';
}
let html = `
<div class="lc-timeline" style="padding: 16px; border-bottom: 1px solid #3c3c3c;">
<h3 style="margin: 0 0 12px 0; color: #cccccc; font-size: 14px; font-weight: 600;">
Execution Timeline
</h3>
`;
// Get time range
const minTime = Math.min(...this.performanceRecording.map(r => r.startTime));
const maxTime = Math.max(...this.performanceRecording.map(r => r.endTime));
const timeRange = maxTime - minTime;
// Render timeline bars
html += '<div style="position: relative; height: 200px; margin-top: 12px; border: 1px solid #3c3c3c; border-radius: 4px; padding: 8px;">';
let yOffset = 0;
const barHeight = 16;
const maxBars = Math.floor(200 / barHeight);
for (const record of this.performanceRecording.slice(0, maxBars)) {
const leftPercent = ((record.startTime - minTime) / timeRange) * 100;
const widthPercent = (record.duration / timeRange) * 100;
const color = record.type === 'action' ? '#dcdcaa' : '#569cd6';
const componentName = this.components.get(record.componentId)?.name || record.componentId.substring(0, 8);
const label = record.type === 'action' ? record.actionName : 'render';
html += `
<div
style="
position: absolute;
left: ${leftPercent}%;
top: ${yOffset}px;
width: ${widthPercent}%;
height: ${barHeight - 2}px;
background: ${color};
border-radius: 2px;
font-size: 10px;
color: #1e1e1e;
padding: 2px 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
"
title="${componentName}.${label}() - ${record.duration.toFixed(2)}ms"
>
${componentName}.${label}()
</div>
`;
yOffset += barHeight;
}
html += '</div>';
// Time labels
html += `
<div style="display: flex; justify-content: space-between; margin-top: 4px; font-size: 10px; color: #858585;">
<span>0ms</span>
<span>${(timeRange / 2).toFixed(2)}ms</span>
<span>${timeRange.toFixed(2)}ms</span>
</div>
`;
html += '</div>';
return html;
}
/**
* Render memory usage chart
*/
renderMemoryChart() {
if (this.memorySnapshots.length === 0) {
return '';
}
let html = `
<div class="lc-memory-chart" style="padding: 16px;">
<h3 style="margin: 0 0 12px 0; color: #cccccc; font-size: 14px; font-weight: 600;">
Memory Usage
</h3>
`;
// Get current memory stats
const current = this.memorySnapshots[this.memorySnapshots.length - 1];
const initial = this.memorySnapshots[0];
const delta = current.usedJSHeapSize - initial.usedJSHeapSize;
const deltaPercent = (delta / initial.usedJSHeapSize) * 100;
html += `
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 16px; font-size: 12px;">
<div>
<div style="color: #858585; margin-bottom: 4px;">Used Heap</div>
<div style="color: #4ec9b0; font-size: 18px; font-weight: 600;">${this.formatBytes(current.usedJSHeapSize)}</div>
</div>
<div>
<div style="color: #858585; margin-bottom: 4px;">Total Heap</div>
<div style="color: #569cd6; font-size: 18px; font-weight: 600;">${this.formatBytes(current.totalJSHeapSize)}</div>
</div>
<div>
<div style="color: #858585; margin-bottom: 4px;">Heap Limit</div>
<div style="color: #dcdcaa; font-size: 18px; font-weight: 600;">${this.formatBytes(current.jsHeapSizeLimit)}</div>
</div>
<div>
<div style="color: #858585; margin-bottom: 4px;">Delta</div>
<div style="color: ${delta > 0 ? '#f48771' : '#4ec9b0'}; font-size: 18px; font-weight: 600;">
${delta > 0 ? '+' : ''}${this.formatBytes(delta)}
</div>
</div>
</div>
`;
// Render memory chart
const maxMemory = Math.max(...this.memorySnapshots.map(s => s.usedJSHeapSize));
const chartHeight = 100;
html += '<div style="position: relative; height: 100px; border: 1px solid #3c3c3c; border-radius: 4px; background: #252526;">';
// Draw memory line chart
let points = '';
const step = 100 / (this.memorySnapshots.length - 1);
for (let i = 0; i < this.memorySnapshots.length; i++) {
const snapshot = this.memorySnapshots[i];
const x = i * step;
const y = chartHeight - (snapshot.usedJSHeapSize / maxMemory) * chartHeight;
points += `${x},${y} `;
}
html += `
<svg width="100%" height="100" style="position: absolute; top: 0; left: 0;">
<polyline
points="${points}"
style="fill: none; stroke: #4ec9b0; stroke-width: 2;"
/>
</svg>
`;
html += '</div>';
html += '</div>';
return html;
}
/**
* Format bytes to human-readable string
*/
formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
/**
* Clear network log
*/
clearNetworkLog() {
this.networkLog = [];
this.renderNetworkLog();
}
/**
* Render network log
*/
renderNetworkLog() {
const container = this.overlay.querySelector('[data-content="network"]');
if (!container) return;
if (this.networkLog.length === 0) {
container.innerHTML = '<div style="color: #969696; text-align: center; padding: 40px;">No requests logged</div>';
return;
}
let html = '<div class="lc-network-log">';
for (const request of this.networkLog) {
const time = new Date(request.timestamp).toLocaleTimeString();
html += `
<div class="lc-log-entry">
<div class="lc-log-header">
<span class="lc-log-time">${time}</span>
<span class="lc-log-method">${request.method}</span>
<span class="lc-log-url">${request.url}</span>
<span class="lc-log-status">${request.status}</span>
<span class="lc-log-duration">${request.duration.toFixed(2)}ms</span>
</div>
</div>
`;
}
html += '</div>';
container.innerHTML = html;
}
/**
* Initialize DOM badges for all components
*/
initializeDomBadges() {
// Observe DOM for new components
const observer = new MutationObserver(() => {
this.updateDomBadges();
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['data-component-id']
});
// Initial badge creation
this.updateDomBadges();
}
/**
* Update DOM badges for all LiveComponents
*/
updateDomBadges() {
if (!this.badgesEnabled) return;
// Find all LiveComponent elements
document.querySelectorAll('[data-component-id]').forEach(element => {
const componentId = element.dataset.componentId;
// Skip if badge already exists
if (this.domBadges.has(componentId)) {
const existingBadge = this.domBadges.get(componentId);
// Update badge position if element moved
this.updateBadgePosition(existingBadge, element);
return;
}
// Create new badge
this.createDomBadge(componentId, element);
});
// Clean up badges for removed components
this.cleanupRemovedBadges();
}
/**
* Create DOM badge for a component
*/
createDomBadge(componentId, element) {
const badge = document.createElement('div');
badge.className = 'lc-dom-badge';
badge.dataset.componentId = componentId;
const componentName = element.dataset.componentName || 'Unknown';
badge.innerHTML = `
<div class="lc-badge-header">
<span class="lc-badge-icon">⚡</span>
<span class="lc-badge-name">${componentName}</span>
<span class="lc-badge-id">#${componentId.substring(0, 8)}</span>
</div>
<div class="lc-badge-activity">
<span class="lc-badge-actions">0 actions</span>
</div>
`;
// Position badge relative to element
this.positionBadge(badge, element);
// Add interaction handlers
badge.addEventListener('click', (e) => {
e.stopPropagation();
this.highlightComponent(componentId);
this.focusComponentInDevTools(componentId);
});
badge.addEventListener('mouseenter', () => {
this.highlightElement(element);
});
badge.addEventListener('mouseleave', () => {
this.unhighlightElement(element);
});
document.body.appendChild(badge);
this.domBadges.set(componentId, { badge, element, actionCount: 0 });
}
/**
* Position badge relative to component element
*/
positionBadge(badge, element) {
const rect = element.getBoundingClientRect();
badge.style.position = 'fixed';
badge.style.top = `${rect.top + window.scrollY}px`;
badge.style.left = `${rect.left + window.scrollX}px`;
badge.style.zIndex = '999998';
}
/**
* Update badge position when element moves
*/
updateBadgePosition(badgeData, element) {
const rect = element.getBoundingClientRect();
badgeData.badge.style.top = `${rect.top + window.scrollY}px`;
badgeData.badge.style.left = `${rect.left + window.scrollX}px`;
}
/**
* Update badge activity counter
*/
updateBadgeActivity(componentId) {
const badgeData = this.domBadges.get(componentId);
if (!badgeData) return;
badgeData.actionCount++;
const actionsSpan = badgeData.badge.querySelector('.lc-badge-actions');
if (actionsSpan) {
actionsSpan.textContent = `${badgeData.actionCount} action${badgeData.actionCount !== 1 ? 's' : ''}`;
}
// Flash badge to show activity
badgeData.badge.classList.add('lc-badge-active');
setTimeout(() => {
badgeData.badge.classList.remove('lc-badge-active');
}, 500);
}
/**
* Highlight component element
*/
highlightElement(element) {
element.style.outline = '2px solid #007acc';
element.style.outlineOffset = '2px';
}
/**
* Remove element highlight
*/
unhighlightElement(element) {
element.style.outline = '';
element.style.outlineOffset = '';
}
/**
* Highlight component in DevTools
*/
highlightComponent(componentId) {
if (!this.isOpen) {
this.open();
}
this.switchTab('components');
// Highlight component in tree
const componentItem = this.overlay.querySelector(`[data-component-id="${componentId}"]`);
if (componentItem) {
componentItem.scrollIntoView({ behavior: 'smooth', block: 'center' });
componentItem.style.background = 'rgba(0, 122, 204, 0.2)';
setTimeout(() => {
componentItem.style.background = '';
}, 2000);
}
}
/**
* Focus component in DevTools tree
*/
focusComponentInDevTools(componentId) {
const componentItem = this.overlay.querySelector(`[data-component-id="${componentId}"]`);
if (componentItem) {
// Expand component details
const details = componentItem.querySelector('.lc-component-details');
if (details) {
details.style.display = 'block';
}
}
}
/**
* Clean up badges for removed components
*/
cleanupRemovedBadges() {
const existingComponents = new Set(
Array.from(document.querySelectorAll('[data-component-id]'))
.map(el => el.dataset.componentId)
);
for (const [componentId, badgeData] of this.domBadges) {
if (!existingComponents.has(componentId)) {
badgeData.badge.remove();
this.domBadges.delete(componentId);
}
}
}
/**
* Toggle DOM badges visibility
*/
toggleBadges() {
this.badgesEnabled = !this.badgesEnabled;
if (this.badgesEnabled) {
// Show all badges
for (const badgeData of this.domBadges.values()) {
badgeData.badge.style.display = 'block';
}
this.updateDomBadges();
} else {
// Hide all badges
for (const badgeData of this.domBadges.values()) {
badgeData.badge.style.display = 'none';
}
}
// Update button state
const badgeBtn = this.overlay?.querySelector('[data-action="toggle-badges"]');
if (badgeBtn) {
badgeBtn.style.opacity = this.badgesEnabled ? '1' : '0.5';
}
}
/**
* Remove all DOM badges
*/
removeAllBadges() {
for (const badgeData of this.domBadges.values()) {
badgeData.badge.remove();
}
this.domBadges.clear();
}
/**
* Intercept Core.emit for event logging
*/
interceptCoreEmit() {
const originalEmit = Core.emit;
Core.emit = (eventName, data) => {
// Log as client-side event (from Core.emit)
this.logEvent(eventName, data, 'client');
return originalEmit.call(Core, eventName, data);
};
}
/**
* Monitor component initialization
*/
monitorComponentInit() {
Core.on('component:initialized', (data) => {
this.refreshComponents();
});
Core.on('component:destroyed', (data) => {
this.refreshComponents();
});
}
/**
* Monitor network requests
*/
monitorNetworkRequests() {
// Intercept fetch
const originalFetch = window.fetch;
window.fetch = async (...args) => {
const startTime = performance.now();
const url = typeof args[0] === 'string' ? args[0] : args[0].url;
const method = args[1]?.method || 'GET';
try {
const response = await originalFetch(...args);
const duration = performance.now() - startTime;
this.networkLog.unshift({
timestamp: Date.now(),
method,
url,
status: response.status,
duration
});
// Keep only last 50 requests
if (this.networkLog.length > 50) {
this.networkLog.pop();
}
if (this.activeTab === 'network') {
this.renderNetworkLog();
}
return response;
} catch (error) {
const duration = performance.now() - startTime;
this.networkLog.unshift({
timestamp: Date.now(),
method,
url,
status: 'Error',
duration
});
throw error;
}
};
}
}
// Auto-initialize if enabled
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
window.__liveComponentDevTools = new LiveComponentDevTools();
});
} else {
window.__liveComponentDevTools = new LiveComponentDevTools();
}