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'; import { DebugConsoleResizeService } from './services/debug-console-resize.service'; import { DebugConsoleExportService, type DebugExportFormat } from './services/debug-console-export.service'; import { DebugConsoleEnvironmentService } from './services/debug-console-environment.service'; 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 resizeService = inject(DebugConsoleResizeService); readonly exportService = inject(DebugConsoleExportService); readonly envService = inject(DebugConsoleEnvironmentService); 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 = this.resizeService.panelHeight; readonly panelWidth = this.resizeService.panelWidth; readonly panelLeft = this.resizeService.panelLeft; readonly panelTop = this.resizeService.panelTop; 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); constructor() { this.resizeService.syncBounds(this.detached()); 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 { this.resizeService.onMouseMove(event, this.detached()); } @HostListener('window:mouseup') onResizeEnd(): void { this.resizeService.onMouseUp(); } @HostListener('window:resize') onWindowResize(): void { this.resizeService.syncBounds(this.detached()); } 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); } exportLogs(format: DebugExportFormat): void { const env = this.envService.getEnvironment(); const name = this.envService.getFilenameSafeDisplayName(); this.exportService.exportLogs( this.filteredEntries(), format, env, name ); } exportNetwork(format: DebugExportFormat): void { const env = this.envService.getEnvironment(); const name = this.envService.getFilenameSafeDisplayName(); this.exportService.exportNetwork( this.networkSnapshot(), format, env, name ); } toggleDetached(): void { const nextDetached = !this.detached(); this.detached.set(nextDetached); this.resizeService.syncBounds(nextDetached); if (nextDetached) this.resizeService.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(); } startTopResize(event: MouseEvent): void { this.resizeService.startTopResize(event); } startBottomResize(event: MouseEvent): void { this.resizeService.startBottomResize(event); } startLeftResize(event: MouseEvent): void { this.resizeService.startLeftResize(event); } startRightResize(event: MouseEvent): void { this.resizeService.startRightResize(event); } startCornerResize(event: MouseEvent): void { this.resizeService.startCornerResize(event); } startDrag(event: MouseEvent): void { if (!this.detached()) return; this.resizeService.startDrag(event); } formatBadgeCount(count: number): string { if (count > 99) return '99+'; return count.toString(); } }