import { Component, inject, signal, OnInit, OnDestroy, ViewChild, ElementRef, effect } from '@angular/core'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { FormsModule } from '@angular/forms'; import { ApiService } from '../../core/services/api.service'; import { ToastService } from '../../core/services/toast.service'; import { LogStreamService } from '../../core/services/log-stream.service'; import { IService, IServiceUpdate } from '../../core/types/api.types'; import { CardComponent, CardHeaderComponent, CardTitleComponent, CardDescriptionComponent, CardContentComponent, CardFooterComponent, } from '../../ui/card/card.component'; import { ButtonComponent } from '../../ui/button/button.component'; import { BadgeComponent } from '../../ui/badge/badge.component'; import { InputComponent } from '../../ui/input/input.component'; import { LabelComponent } from '../../ui/label/label.component'; import { SkeletonComponent } from '../../ui/skeleton/skeleton.component'; import { SeparatorComponent } from '../../ui/separator/separator.component'; import { DialogComponent, DialogHeaderComponent, DialogTitleComponent, DialogDescriptionComponent, DialogFooterComponent, } from '../../ui/dialog/dialog.component'; @Component({ selector: 'app-service-detail', standalone: true, imports: [ FormsModule, RouterLink, CardComponent, CardHeaderComponent, CardTitleComponent, CardDescriptionComponent, CardContentComponent, CardFooterComponent, ButtonComponent, BadgeComponent, InputComponent, LabelComponent, SkeletonComponent, SeparatorComponent, DialogComponent, DialogHeaderComponent, DialogTitleComponent, DialogDescriptionComponent, DialogFooterComponent, ], template: `
Back to Services @if (loading() && !service()) { } @else if (service()) {

{{ service()!.name }}

{{ service()!.status }}
}
@if (loading() && !service()) {
@for (_ of [1,2,3,4]; track $index) { }
} @else if (service()) {
Service Details @if (!editMode()) { } @if (editMode()) {
} @else {
Image
{{ service()!.image }}
Port
{{ service()!.port }}
Domain
@if (service()!.domain) { {{ service()!.domain }} } @else { Not configured }
@if (service()!.containerID) {
Container ID
{{ service()!.containerID?.slice(0, 12) }}
}
Created
{{ formatDate(service()!.createdAt) }}
Updated
{{ formatDate(service()!.updatedAt) }}
}
Actions Manage service state
@if (service()!.status === 'stopped' || service()!.status === 'failed') { } @if (service()!.status === 'running') { }
@if (service()!.envVars && getEnvKeys(service()!.envVars).length > 0) { Environment Variables
@for (key of getEnvKeys(service()!.envVars); track key) {
{{ key }}: {{ service()!.envVars[key] }}
}
} @if (service()!.useOneboxRegistry) { Onebox Registry Push images directly to this service
Repository
{{ service()!.registryRepository }}
Tag
{{ service()!.registryImageTag || 'latest' }}
@if (service()!.registryToken) {
Push Token
}
Auto-update on push
{{ service()!.autoUpdateOnPush ? 'Enabled' : 'Disabled' }}
}
Logs @if (logStream.isStreaming()) { Live streaming } @else { Container logs }
@if (logStream.isStreaming()) { } @else { }
@if (logStream.state().error) {

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

} @else if (logStream.logs().length > 0) { @for (line of logStream.logs(); track $index) {
{{ line }}
} } @else if (logStream.isStreaming()) {

Waiting for logs...

} @else {

Click "Stream" to start live log streaming

}
}
Delete Service Are you sure you want to delete "{{ service()?.name }}"? This action cannot be undone. `, }) export class ServiceDetailComponent implements OnInit, OnDestroy { private route = inject(ActivatedRoute); private router = inject(Router); private api = inject(ApiService); private toast = inject(ToastService); logStream = inject(LogStreamService); @ViewChild('logContainer') logContainer!: ElementRef; service = signal(null); loading = signal(false); actionLoading = signal(false); editMode = signal(false); deleteDialogOpen = signal(false); autoScroll = true; editForm: IServiceUpdate = {}; constructor() { // Auto-scroll when new logs arrive effect(() => { const logs = this.logStream.logs(); if (logs.length > 0 && this.autoScroll && this.logContainer?.nativeElement) { setTimeout(() => { const container = this.logContainer.nativeElement; container.scrollTop = container.scrollHeight; }, 0); } }); } ngOnInit(): void { const name = this.route.snapshot.paramMap.get('name'); if (name) { this.loadService(name); } } ngOnDestroy(): void { this.logStream.disconnect(); } async loadService(name: string): Promise { this.loading.set(true); try { const response = await this.api.getService(name); if (response.success && response.data) { this.service.set(response.data); this.editForm = { image: response.data.image, port: response.data.port, domain: response.data.domain, }; } else { this.toast.error(response.error || 'Service not found'); this.router.navigate(['/services']); } } catch { this.toast.error('Failed to load service'); } finally { this.loading.set(false); } } startLogStream(): void { const name = this.service()?.name; if (name) { this.logStream.connect(name); } } stopLogStream(): void { this.logStream.disconnect(); } clearLogs(): void { this.logStream.clearLogs(); } 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'; } } formatDate(timestamp: number): string { return new Date(timestamp).toLocaleString(); } getEnvKeys(envVars: Record): string[] { return Object.keys(envVars); } cancelEdit(): void { this.editMode.set(false); if (this.service()) { this.editForm = { image: this.service()!.image, port: this.service()!.port, domain: this.service()!.domain, }; } } async saveChanges(): Promise { const name = this.service()?.name; if (!name) return; this.actionLoading.set(true); try { const response = await this.api.updateService(name, this.editForm); if (response.success) { this.toast.success('Service updated'); this.editMode.set(false); this.loadService(name); } else { this.toast.error(response.error || 'Failed to update service'); } } catch { this.toast.error('Failed to update service'); } finally { this.actionLoading.set(false); } } async startService(): Promise { const name = this.service()?.name; if (!name) return; this.actionLoading.set(true); try { const response = await this.api.startService(name); if (response.success) { this.toast.success('Service started'); this.loadService(name); } else { this.toast.error(response.error || 'Failed to start service'); } } catch { this.toast.error('Failed to start service'); } finally { this.actionLoading.set(false); } } async stopService(): Promise { const name = this.service()?.name; if (!name) return; this.actionLoading.set(true); try { const response = await this.api.stopService(name); if (response.success) { this.toast.success('Service stopped'); this.loadService(name); } else { this.toast.error(response.error || 'Failed to stop service'); } } catch { this.toast.error('Failed to stop service'); } finally { this.actionLoading.set(false); } } async restartService(): Promise { const name = this.service()?.name; if (!name) return; this.actionLoading.set(true); try { const response = await this.api.restartService(name); if (response.success) { this.toast.success('Service restarted'); this.loadService(name); } else { this.toast.error(response.error || 'Failed to restart service'); } } catch { this.toast.error('Failed to restart service'); } finally { this.actionLoading.set(false); } } async deleteService(): Promise { const name = this.service()?.name; if (!name) return; this.actionLoading.set(true); try { const response = await this.api.deleteService(name); if (response.success) { this.toast.success('Service deleted'); this.router.navigate(['/services']); } else { this.toast.error(response.error || 'Failed to delete service'); } } catch { this.toast.error('Failed to delete service'); } finally { this.actionLoading.set(false); this.deleteDialogOpen.set(false); } } copyToken(): void { const token = this.service()?.registryToken; if (token) { navigator.clipboard.writeText(token); this.toast.success('Token copied to clipboard'); } } }