import { Component, inject, signal, OnInit, OnDestroy, ViewChild, ElementRef, effect } from '@angular/core'; import { ApiService } from '../../core/services/api.service'; import { ToastService } from '../../core/services/toast.service'; import { NetworkLogStreamService } from '../../core/services/network-log-stream.service'; import type { INetworkTarget, INetworkStats, ICaddyAccessLog } from '../../core/types/api.types'; import { CardComponent, CardHeaderComponent, CardTitleComponent, CardDescriptionComponent, CardContentComponent, } from '../../ui/card/card.component'; import { ButtonComponent } from '../../ui/button/button.component'; import { BadgeComponent } from '../../ui/badge/badge.component'; import { SkeletonComponent } from '../../ui/skeleton/skeleton.component'; import { TableComponent, TableHeaderComponent, TableBodyComponent, TableRowComponent, TableHeadComponent, TableCellComponent, } from '../../ui/table/table.component'; @Component({ selector: 'app-network', standalone: true, imports: [ CardComponent, CardHeaderComponent, CardTitleComponent, CardDescriptionComponent, CardContentComponent, ButtonComponent, BadgeComponent, SkeletonComponent, TableComponent, TableHeaderComponent, TableBodyComponent, TableRowComponent, TableHeadComponent, TableCellComponent, ], template: `

Network

Traffic targets and access logs

@if (loading() && !stats()) {
@for (_ of [1,2,3,4]; track $index) { }
} @else if (stats()) {
Proxy Status {{ stats()!.proxy.running ? 'Running' : 'Stopped' }} Routes
{{ stats()!.proxy.routes }}
Certificates
{{ stats()!.proxy.certificates }}
Targets
{{ targets().length }}
} Traffic Targets Services, registry, and platform services with their routing info. Click to filter logs. @if (targets().length === 0 && !loading()) {

No traffic targets configured

} @else { Type Name Domain Target Status @for (target of targets(); track target.name) { {{ target.type }} {{ target.name }} @if (target.domain) { {{ target.domain }} } @else { - } {{ target.targetHost }}:{{ target.targetPort }} {{ target.status }} } }
Access Logs @if (networkLogStream.isStreaming()) { Live streaming @if (activeFilter()) { - filtered by {{ activeFilter() }} } } @else { Real-time Caddy access logs }
@if (activeFilter()) { } @if (networkLogStream.isStreaming()) { } @else { }
@if (networkLogStream.state().error) {

Error: {{ networkLogStream.state().error }}

} @else if (networkLogStream.logs().length > 0) { @for (log of networkLogStream.logs(); track $index) {
{{ formatLog(log) }}
} } @else if (networkLogStream.isStreaming()) {

Waiting for access logs...

} @else {

Click "Stream" to start live access log streaming

}
`, }) export class NetworkComponent implements OnInit, OnDestroy { private api = inject(ApiService); private toast = inject(ToastService); networkLogStream = inject(NetworkLogStreamService); @ViewChild('logContainer') logContainer!: ElementRef; targets = signal([]); stats = signal(null); loading = signal(false); activeFilter = signal(null); constructor() { // Auto-scroll when new logs arrive effect(() => { const logs = this.networkLogStream.logs(); if (logs.length > 0 && this.logContainer?.nativeElement) { setTimeout(() => { const container = this.logContainer.nativeElement; container.scrollTop = container.scrollHeight; }, 0); } }); } ngOnInit(): void { this.loadData(); } ngOnDestroy(): void { this.networkLogStream.disconnect(); } async loadData(): Promise { this.loading.set(true); try { const [targetsResponse, statsResponse] = await Promise.all([ this.api.getNetworkTargets(), this.api.getNetworkStats(), ]); if (targetsResponse.success && targetsResponse.data) { this.targets.set(targetsResponse.data); } if (statsResponse.success && statsResponse.data) { this.stats.set(statsResponse.data); } } catch (err) { this.toast.error('Failed to load network data'); } finally { this.loading.set(false); } } onTargetClick(target: INetworkTarget): void { if (target.domain) { this.activeFilter.set(target.domain); this.networkLogStream.setFilter({ domain: target.domain }); // Start streaming if not already if (!this.networkLogStream.isStreaming()) { this.startLogStream(); } } } clearFilter(): void { this.activeFilter.set(null); this.networkLogStream.setFilter(null); } startLogStream(): void { const filter = this.activeFilter() ? { domain: this.activeFilter()! } : undefined; this.networkLogStream.connect(filter); } stopLogStream(): void { this.networkLogStream.disconnect(); } clearLogs(): void { this.networkLogStream.clearLogs(); } getTypeVariant(type: string): 'default' | 'secondary' | 'outline' { switch (type) { case 'service': return 'default'; case 'registry': return 'secondary'; case 'platform': return 'outline'; default: return 'secondary'; } } getStatusVariant(status: string): 'success' | 'destructive' | 'warning' | 'secondary' { switch (status) { case 'running': return 'success'; case 'stopped': return 'secondary'; case 'failed': return 'destructive'; case 'starting': case 'stopping': return 'warning'; default: return 'secondary'; } } getLogClass(status: number): string { if (status >= 500) return 'text-red-400'; if (status >= 400) return 'text-yellow-400'; if (status >= 300) return 'text-blue-400'; return 'text-green-400'; } formatLog(log: ICaddyAccessLog): string { const time = new Date(log.ts * 1000).toLocaleTimeString(); const duration = log.duration < 1 ? `${(log.duration * 1000).toFixed(1)}ms` : `${log.duration.toFixed(2)}s`; const size = this.formatBytes(log.size); const method = log.request.method.padEnd(7); const status = String(log.status).padStart(3); const host = log.request.host.substring(0, 30).padEnd(30); const uri = log.request.uri.substring(0, 40); return `${time} ${status} ${method} ${host} ${uri.padEnd(40)} ${duration.padStart(8)} ${size.padStart(8)} ${log.request.remote_ip}`; } formatBytes(bytes: number): string { 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 `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`; } }