import { Component, HostListener, computed, effect, inject, input, signal } from '@angular/core'; import { CommonModule } from '@angular/common'; import { NgIcon, provideIcons } from '@ng-icons/core'; import { lucideBug } from '@ng-icons/lucide'; import { DebuggingService, type DebugLogLevel } from '../../../core/services/debugging.service'; import { DebugConsoleEntryListComponent } from './debug-console-entry-list/debug-console-entry-list.component'; import { DebugConsoleNetworkMapComponent } from './debug-console-network-map/debug-console-network-map.component'; import { DebugConsoleToolbarComponent } from './debug-console-toolbar/debug-console-toolbar.component'; type DebugLevelState = Record; type DebugConsoleTab = 'logs' | 'network'; type DebugConsoleLauncherVariant = 'floating' | 'inline' | 'compact'; @Component({ selector: 'app-debug-console', standalone: true, imports: [ CommonModule, NgIcon, DebugConsoleEntryListComponent, DebugConsoleNetworkMapComponent, DebugConsoleToolbarComponent ], viewProviders: [ provideIcons({ lucideBug }) ], templateUrl: './debug-console.component.html', host: { style: 'display: contents;', 'data-debug-console-root': 'true' } }) export class DebugConsoleComponent { readonly debugging = inject(DebuggingService); readonly entries = this.debugging.entries; readonly isOpen = this.debugging.isConsoleOpen; readonly networkSnapshot = this.debugging.networkSnapshot; readonly launcherVariant = input('floating'); readonly showLauncher = input(true); readonly showPanel = input(true); readonly activeTab = signal('logs'); readonly detached = signal(false); readonly searchTerm = signal(''); readonly selectedSource = signal('all'); readonly autoScroll = signal(true); readonly panelHeight = signal(360); readonly panelWidth = signal(832); readonly panelLeft = signal(0); readonly panelTop = signal(0); readonly levelState = signal({ event: true, info: true, warn: true, error: true, debug: true }); readonly sourceOptions = computed(() => { return Array.from(new Set(this.entries().map((entry) => entry.source))).sort(); }); readonly filteredEntries = computed(() => { const searchTerm = this.searchTerm().trim() .toLowerCase(); const selectedSource = this.selectedSource(); const levelState = this.levelState(); return this.entries().filter((entry) => { if (!levelState[entry.level]) return false; if (selectedSource !== 'all' && entry.source !== selectedSource) return false; if (!searchTerm) return true; return [ entry.message, entry.source, entry.level, entry.timeLabel, entry.dateTimeLabel, entry.payloadText || '' ].some((value) => value.toLowerCase().includes(searchTerm)); }); }); readonly levelCounts = computed>(() => { const counts: Record = { event: 0, info: 0, warn: 0, error: 0, debug: 0 }; for (const entry of this.entries()) { counts[entry.level] += entry.count; } return counts; }); readonly visibleCount = computed(() => { return this.filteredEntries().reduce((sum, entry) => sum + entry.count, 0); }); readonly badgeCount = computed(() => { const counts = this.levelCounts(); return counts.error > 0 ? counts.error : this.entries().reduce((sum, entry) => sum + entry.count, 0); }); readonly hasErrors = computed(() => this.levelCounts().error > 0); readonly networkSummary = computed(() => this.networkSnapshot().summary); private dragging = false; private resizingHeight = false; private resizingWidth = false; private resizeOriginY = 0; private resizeOriginX = 0; private resizeOriginHeight = 360; private resizeOriginWidth = 832; private panelOriginLeft = 0; private panelOriginTop = 0; constructor() { this.syncPanelBounds(); effect(() => { const selectedSource = this.selectedSource(); const sourceOptions = this.sourceOptions(); if (selectedSource !== 'all' && !sourceOptions.includes(selectedSource)) this.selectedSource.set('all'); }); } @HostListener('window:mousemove', ['$event']) onResizeMove(event: MouseEvent): void { if (this.dragging) { this.updateDetachedPosition(event); return; } if (this.resizingWidth) { this.updatePanelWidth(event); return; } if (!this.resizingHeight) return; this.updatePanelHeight(event); } @HostListener('window:mouseup') onResizeEnd(): void { this.dragging = false; this.resizingHeight = false; this.resizingWidth = false; } @HostListener('window:resize') onWindowResize(): void { this.syncPanelBounds(); } toggleConsole(): void { this.debugging.toggleConsole(); } closeConsole(): void { this.debugging.closeConsole(); } updateSearchTerm(value: string): void { this.searchTerm.set(value); } updateSelectedSource(source: string): void { this.selectedSource.set(source); } setActiveTab(tab: DebugConsoleTab): void { this.activeTab.set(tab); } toggleDetached(): void { const nextDetached = !this.detached(); this.detached.set(nextDetached); this.syncPanelBounds(); if (nextDetached) this.initializeDetachedPosition(); } toggleLevel(level: DebugLogLevel): void { this.levelState.update((current) => ({ ...current, [level]: !current[level] })); } toggleAutoScroll(): void { this.autoScroll.update((enabled) => !enabled); } clearLogs(): void { this.debugging.clear(); } startResize(event: MouseEvent): void { event.preventDefault(); event.stopPropagation(); this.resizingHeight = true; this.resizeOriginY = event.clientY; this.resizeOriginHeight = this.panelHeight(); this.panelOriginTop = this.panelTop(); } startWidthResize(event: MouseEvent): void { event.preventDefault(); event.stopPropagation(); this.resizingWidth = true; this.resizeOriginX = event.clientX; this.resizeOriginWidth = this.panelWidth(); this.panelOriginLeft = this.panelLeft(); } startDrag(event: MouseEvent): void { if (!this.detached()) return; event.preventDefault(); event.stopPropagation(); this.dragging = true; this.resizeOriginX = event.clientX; this.resizeOriginY = event.clientY; this.panelOriginLeft = this.panelLeft(); this.panelOriginTop = this.panelTop(); } formatBadgeCount(count: number): string { if (count > 99) return '99+'; return count.toString(); } private updatePanelHeight(event: MouseEvent): void { const delta = this.resizeOriginY - event.clientY; const nextHeight = this.clampPanelHeight(this.resizeOriginHeight + delta); this.panelHeight.set(nextHeight); if (!this.detached()) return; const originBottom = this.panelOriginTop + this.resizeOriginHeight; const maxTop = this.getMaxPanelTop(nextHeight); this.panelTop.set(this.clampValue(originBottom - nextHeight, 16, maxTop)); } private updatePanelWidth(event: MouseEvent): void { const delta = this.resizeOriginX - event.clientX; const nextWidth = this.clampPanelWidth(this.resizeOriginWidth + delta); this.panelWidth.set(nextWidth); if (!this.detached()) return; const originRight = this.panelOriginLeft + this.resizeOriginWidth; const maxLeft = this.getMaxPanelLeft(nextWidth); this.panelLeft.set(this.clampValue(originRight - nextWidth, 16, maxLeft)); } private updateDetachedPosition(event: MouseEvent): void { const nextLeft = this.panelOriginLeft + (event.clientX - this.resizeOriginX); const nextTop = this.panelOriginTop + (event.clientY - this.resizeOriginY); this.panelLeft.set(this.clampValue(nextLeft, 16, this.getMaxPanelLeft(this.panelWidth()))); this.panelTop.set(this.clampValue(nextTop, 16, this.getMaxPanelTop(this.panelHeight()))); } private initializeDetachedPosition(): void { if (this.panelLeft() > 0 || this.panelTop() > 0) { this.clampDetachedPosition(); return; } this.panelLeft.set(this.getMaxPanelLeft(this.panelWidth())); this.panelTop.set(this.clampValue(window.innerHeight - this.panelHeight() - 96, 16, this.getMaxPanelTop(this.panelHeight()))); } private clampPanelHeight(height: number): number { const maxHeight = this.detached() ? Math.max(260, window.innerHeight - 32) : Math.floor(window.innerHeight * 0.75); return Math.min(Math.max(height, 260), maxHeight); } private clampPanelWidth(width: number): number { const maxWidth = Math.max(360, window.innerWidth - 32); const minWidth = Math.min(460, maxWidth); return Math.min(Math.max(width, minWidth), maxWidth); } private clampDetachedPosition(): void { this.panelLeft.set(this.clampValue(this.panelLeft(), 16, this.getMaxPanelLeft(this.panelWidth()))); this.panelTop.set(this.clampValue(this.panelTop(), 16, this.getMaxPanelTop(this.panelHeight()))); } private getMaxPanelLeft(width: number): number { return Math.max(16, window.innerWidth - width - 16); } private getMaxPanelTop(height: number): number { return Math.max(16, window.innerHeight - height - 16); } private syncPanelBounds(): void { this.panelWidth.update((width) => this.clampPanelWidth(width)); this.panelHeight.update((height) => this.clampPanelHeight(height)); if (this.detached()) this.clampDetachedPosition(); } private clampValue(value: number, min: number, max: number): number { return Math.min(Math.max(value, min), max); } }