- 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.
1796 lines
58 KiB
JavaScript
1796 lines
58 KiB
JavaScript
/**
|
||
* 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();
|
||
}
|