first commit

This commit is contained in:
Mateusz Gruszczyński
2026-04-12 21:26:12 +02:00
commit ff7dbcb4e4
123 changed files with 27749 additions and 0 deletions

View File

@@ -0,0 +1,286 @@
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';
interface BackupItem {
id: number;
file_name: string;
backup_type: 'export' | 'binary';
created_at: string;
}
interface ConnectionSnapshot {
success: boolean;
tested_at: string;
hostname: string;
model: string;
version?: string | null;
uptime: string;
error?: string | 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: any;
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 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;
}
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: any) => {
this.routerItem = routerItem;
this.connection = this.mapStoredConnection(routerItem);
});
this.api.http.get<BackupItem[]>(`${this.api.baseUrl}/backups/router/${this.routerId}`).subscribe((r) => (this.backups = r));
}
runExport() {
if (this.exporting) {
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<ConnectionSnapshot>(`${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<BackupDiffResponse>(`${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) {
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<any>(`${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: any): 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
};
}
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,
};
}
private openBlob(response: HttpResponse<Blob>, 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);
}
}