import { CommonModule } from '@angular/common'; import { HttpResponse } from '@angular/common/http'; import { Component, OnInit, inject } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { ButtonModule } from 'primeng/button'; import { DialogModule } from 'primeng/dialog'; import { TableModule } from 'primeng/table'; import { TagModule } from 'primeng/tag'; import { ApiService } from '../../core/services/api.service'; import { UiService } from '../../core/services/ui.service'; import { PageHeaderComponent } from '../../shared/ui/page-header.component'; import { SectionCardComponent } from '../../shared/ui/section-card.component'; import { StatCardComponent } from '../../shared/ui/stat-card.component'; type DeviceType = 'routeros' | 'switchos'; interface DeviceItem { id: number; name: string; host: string; port: number; device_type: DeviceType; effective_username?: string | null; supports_export: boolean; supports_restore_upload: boolean; last_connection_status?: boolean | null; last_connection_tested_at?: string | null; last_connection_error?: string | null; last_connection_hostname?: string | null; last_connection_model?: string | null; last_connection_version?: string | null; last_connection_uptime?: string | null; last_connection_transport?: string | null; last_connection_server?: string | null; last_connection_auth_mode?: string | null; last_connection_http_status?: string | null; last_connection_backup_available?: boolean | null; } interface BackupItem { id: number; file_name: string; backup_type: 'export' | 'binary'; created_at: string; device_type: DeviceType; } interface ConnectionSnapshot { success: boolean; tested_at: string; hostname: string; model: string; version?: string | null; uptime: string; error?: string | null; transport?: string | null; server?: string | null; auth_mode?: string | null; http_status?: string | null; backup_available?: boolean | null; } interface BackupDiffStats { added: number; removed: number; modified: number; context: number; } interface BackupDiffResponse { left_backup_id: number; right_backup_id: number; left_file_name?: string | null; right_file_name?: string | null; diff_text: string; stats?: BackupDiffStats | null; } @Component({ standalone: true, imports: [CommonModule, TranslateModule, ButtonModule, DialogModule, TableModule, TagModule, PageHeaderComponent, SectionCardComponent, StatCardComponent], templateUrl: './router-detail-page.component.html' }) export class RouterDetailPageComponent implements OnInit { private readonly route = inject(ActivatedRoute); private readonly api = inject(ApiService); private readonly router = inject(Router); private readonly ui = inject(UiService); routerId!: number; routerItem: DeviceItem | null = null; backups: BackupItem[] = []; connection: ConnectionSnapshot | null = null; exportContent = ''; diffText = ''; previewTitle = ''; previewVisible = false; diffVisible = false; diffData: BackupDiffResponse | null = null; exporting = false; runningBinary = false; testing = false; deletingRouter = false; get isSwitchos(): boolean { return this.routerItem?.device_type === 'switchos'; } get exportBackups(): BackupItem[] { return this.backups.filter((item) => item.backup_type === 'export'); } get binaryBackups(): BackupItem[] { return this.backups.filter((item) => item.backup_type === 'binary'); } get connectionStateLabel(): string { if (!this.connection) { return this.ui.instant('common.idle'); } return this.connection.success ? this.ui.instant('common.ok') : this.ui.instant('common.failed'); } get hasPreview(): boolean { return !!this.exportContent; } get hasDiff(): boolean { return !!this.diffText; } get subtitle(): string { if (!this.routerItem) { return this.ui.instant('routers.detailSubtitle'); } const suffix = this.routerItem.effective_username ? ` ยท ${this.routerItem.effective_username}` : ''; return `${this.routerItem.host}:${this.routerItem.port}${suffix}`; } get deviceTypeLabel(): string { return this.ui.instant(this.isSwitchos ? 'routers.switchos' : 'routers.routeros'); } ngOnInit() { this.routerId = Number(this.route.snapshot.paramMap.get('id')); this.load(); } load() { this.api.http.get(`${this.api.baseUrl}/routers/${this.routerId}`).subscribe((routerItem) => { this.routerItem = routerItem; this.connection = this.mapStoredConnection(routerItem); }); this.api.http.get(`${this.api.baseUrl}/backups/router/${this.routerId}`).subscribe((r) => (this.backups = r)); } runExport() { if (this.exporting || this.isSwitchos) { return; } this.exporting = true; this.api.http.post(`${this.api.baseUrl}/backups/router/${this.routerId}/export`, {}).subscribe({ next: () => { this.ui.success('toast.exportCreated'); this.load(); }, complete: () => { this.exporting = false; } }); } runBinary() { if (this.runningBinary) { return; } this.runningBinary = true; this.api.http.post(`${this.api.baseUrl}/backups/router/${this.routerId}/binary`, {}).subscribe({ next: () => { this.ui.success('toast.binaryCreated'); this.load(); }, complete: () => { this.runningBinary = false; } }); } testConnection() { if (this.testing) { return; } this.testing = true; this.api.http.get(`${this.api.baseUrl}/routers/${this.routerId}/test-connection`).subscribe({ next: (result) => { this.connection = result; this.syncStoredConnection(result); if (result.success) { this.ui.success('toast.connectionSuccessful'); } else { this.ui.error('toast.connectionFailed'); } }, complete: () => { this.testing = false; } }); } compareToLatest(id: number) { const latest = this.exportBackups[0]; if (!latest || latest.id === id) { return; } this.api.http.get(`${this.api.baseUrl}/backups/${id}/diff/${latest.id}`).subscribe((response) => { this.diffData = response; this.diffText = response.diff_text; this.ui.clear(); this.diffVisible = true; }); } async remove(id: number) { const accepted = await this.ui.confirm({ messageKey: 'confirm.deleteBackup', acceptKey: 'common.delete' }); if (!accepted) { return; } this.api.http.delete(`${this.api.baseUrl}/backups/${id}`).subscribe(() => { this.ui.success('toast.backupDeleted'); this.load(); }); } upload(id: number) { if (this.isSwitchos) { return; } this.api.http.post(`${this.api.baseUrl}/backups/router/${this.routerId}/upload/${id}`, {}).subscribe(() => { this.ui.success('toast.binaryUploaded'); }); } async deleteRouter() { if (this.deletingRouter) { return; } const accepted = await this.ui.confirm({ messageKey: 'confirm.deleteRouterWithFiles', acceptKey: 'common.delete' }); if (!accepted) { return; } this.deletingRouter = true; this.api.http.delete(`${this.api.baseUrl}/routers/${this.routerId}`).subscribe({ next: () => this.router.navigate(['/routers']), complete: () => { this.deletingRouter = false; } }); } download(id: number) { this.api.http .get(`${this.api.baseUrl}/backups/${id}/download`, { observe: 'response', responseType: 'blob' }) .subscribe((response) => this.openBlob(response, `backup-${id}`)); } viewExport(id: number) { const backup = this.exportBackups.find((item) => item.id === id); this.api.http.get<{ content: string }>(`${this.api.baseUrl}/backups/${id}/view`).subscribe((r) => { this.exportContent = r.content; this.previewTitle = backup?.file_name || this.ui.instant('routers.previewTitle'); this.ui.clear(); this.previewVisible = true; }); } sendEmail(id: number) { this.api.http.post(`${this.api.baseUrl}/backups/${id}/email`, {}).subscribe(() => { this.ui.success('toast.backupSentEmail'); }); } openPreviewModal() { this.ui.clear(); this.previewVisible = true; } openDiffModal() { this.ui.clear(); this.diffVisible = true; } private mapStoredConnection(routerItem: DeviceItem): ConnectionSnapshot | null { if (!routerItem?.last_connection_tested_at) { return null; } return { success: Boolean(routerItem.last_connection_status), tested_at: routerItem.last_connection_tested_at, hostname: routerItem.last_connection_hostname || routerItem.name, model: routerItem.last_connection_model || 'Unknown', version: routerItem.last_connection_version, uptime: routerItem.last_connection_uptime || 'Unknown', error: routerItem.last_connection_error || null, transport: routerItem.last_connection_transport || null, server: routerItem.last_connection_server || null, auth_mode: routerItem.last_connection_auth_mode || null, http_status: routerItem.last_connection_http_status || null, backup_available: routerItem.last_connection_backup_available ?? null }; } private syncStoredConnection(result: ConnectionSnapshot) { if (!this.routerItem) { return; } this.routerItem = { ...this.routerItem, last_connection_status: result.success, last_connection_tested_at: result.tested_at, last_connection_hostname: result.hostname, last_connection_model: result.model, last_connection_version: result.version, last_connection_uptime: result.uptime, last_connection_error: result.error, last_connection_transport: result.transport, last_connection_server: result.server, last_connection_auth_mode: result.auth_mode, last_connection_http_status: result.http_status, last_connection_backup_available: result.backup_available }; } private openBlob(response: HttpResponse, fallbackName: string) { const disposition = response.headers.get('content-disposition') || ''; const match = disposition.match(/filename="?([^";]+)"?/i); const filename = match?.[1] || fallbackName; const url = URL.createObjectURL(response.body || new Blob()); const link = document.createElement('a'); link.href = url; link.download = filename; link.click(); setTimeout(() => URL.revokeObjectURL(url), 60_000); } }