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]}`;
}
}