switchos support

This commit is contained in:
Mateusz Gruszczyński
2026-04-13 11:59:17 +02:00
parent 5163704b59
commit 4d2356f60b
28 changed files with 1142 additions and 330 deletions

View File

@@ -56,10 +56,9 @@ export class AppComponent {
{ label: 'nav.routers', link: '/routers', icon: 'pi pi-server', exact: false },
{ label: 'nav.files', link: '/files', icon: 'pi pi-folder-open', exact: false },
{ label: 'nav.diffConfigs', link: '/diff-configs', icon: 'pi pi-code', exact: false },
{ label: 'nav.settings', link: '/settings', icon: 'pi pi-cog', exact: false },
{ label: 'nav.logs', link: '/logs', icon: 'pi pi-history', exact: false },
{ label: 'nav.switchosBeta', link: '/switchos-beta', icon: 'pi pi-sitemap', exact: false },
{ label: 'nav.changePassword', link: '/change-password', icon: 'pi pi-lock', exact: false }
{ label: 'nav.changePassword', link: '/change-password', icon: 'pi pi-lock', exact: false },
{ label: 'nav.settings', link: '/settings', icon: 'pi pi-cog', exact: false }
];
get currentPageTitle(): string {
@@ -133,10 +132,6 @@ export class AppComponent {
this.pageLabel = 'logs.title';
return;
}
if (url.startsWith('/switchos-beta')) {
this.pageLabel = 'switchosBeta.title';
return;
}
if (url.startsWith('/change-password')) {
this.pageLabel = 'auth.changePassword';
return;

View File

@@ -11,7 +11,6 @@ import { LogsPageComponent } from './features/logs/logs-page.component';
import { RouterDetailPageComponent } from './features/routers/router-detail-page.component';
import { RoutersPageComponent } from './features/routers/routers-page.component';
import { SettingsPageComponent } from './features/settings/settings-page.component';
import { SwosBetaPageComponent } from './features/swos-beta/swos-beta-page.component';
export const routes: Routes = [
{ path: 'login', component: LoginPageComponent },
@@ -24,6 +23,5 @@ export const routes: Routes = [
{ path: 'diff-configs', canActivate: [authGuard], component: DiffConfigsPageComponent },
{ path: 'settings', canActivate: [authGuard], component: SettingsPageComponent },
{ path: 'logs', canActivate: [authGuard], component: LogsPageComponent },
{ path: 'switchos-beta', canActivate: [authGuard], component: SwosBetaPageComponent },
{ path: '**', redirectTo: '' }
];

View File

@@ -104,7 +104,7 @@
</td>
<td>
<div class="table-primary">{{ item.router_name || item.router_id }}</div>
<small class="table-secondary">ID {{ item.router_id }}</small>
<small class="table-secondary">{{ deviceLabel(item) }} · ID {{ item.router_id }}</small>
</td>
<td><p-tag [value]="item.backup_type === 'export' ? ('files.exportType' | translate) : ('files.binaryType' | translate)" [severity]="item.backup_type === 'export' ? 'success' : 'warning'"></p-tag></td>
<td>
@@ -113,7 +113,7 @@
</td>
<td>
<div class="table-primary">{{ formatBytes(item.file_size) }}</div>
<small class="table-secondary">{{ item.backup_type === 'export' ? '.rsc' : '.backup' }}</small>
<small class="table-secondary">{{ fileExtension(item) }}</small>
</td>
<td>
<div class="table-actions table-actions--stack" *ngIf="item.backup_type === 'export'; else noCompare">
@@ -130,7 +130,7 @@
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" icon="pi pi-download" [label]="'common.download' | translate" (click)="download(item.id)"></button>
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="secondary" icon="pi pi-envelope" [label]="'common.email' | translate" (click)="sendEmail(item.id)"></button>
<button *ngIf="item.backup_type==='export'" pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="info" icon="pi pi-eye" [label]="'common.preview' | translate" (click)="viewExport(item)"></button>
<button *ngIf="item.backup_type==='binary'" pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="help" icon="pi pi-upload" [label]="'common.restore' | translate" (click)="upload(item)"></button>
<button *ngIf="item.backup_type==='binary' && item.device_type==='routeros'" pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="help" icon="pi pi-upload" [label]="'common.restore' | translate" (click)="upload(item)"></button>
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="danger" icon="pi pi-trash" [label]="'common.delete' | translate" (click)="deleteOne(item.id)"></button>
</div>
</td>

View File

@@ -16,10 +16,13 @@ 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 BackupFile {
id: number;
router_id: number;
router_name?: string;
device_type: DeviceType;
file_name: string;
backup_type: 'export' | 'binary';
created_at: string;
@@ -233,6 +236,9 @@ export class FilesPageComponent implements OnInit {
}
upload(item: BackupFile) {
if (item.device_type !== 'routeros') {
return;
}
this.api.http.post(`${this.api.baseUrl}/backups/router/${item.router_id}/upload/${item.id}`, {}).subscribe(() => {
this.ui.success('toast.binaryUploaded');
});
@@ -410,6 +416,15 @@ export class FilesPageComponent implements OnInit {
return `${value.slice(0, 8)}${value.slice(-6)}`;
}
deviceLabel(item: BackupFile): string {
return this.ui.instant(item.device_type === 'switchos' ? 'routers.switchos' : 'routers.routeros');
}
fileExtension(item: BackupFile): string {
const parts = item.file_name.split('.');
return parts.length > 1 ? `.${parts[parts.length - 1]}` : '—';
}
private setComparePair(firstId: number, secondId: number) {
const [left, right] = this.sortPairByDate(firstId, secondId);
this.compareLeftId = left;

View File

@@ -1,21 +1,21 @@
<app-page-header
[eyebrow]="'routers.profileEyebrow' | translate"
[title]="routerItem?.name || ('routers.detailTitle' | translate)"
[subtitle]="routerItem ? routerItem.host + ':' + routerItem.port + ' · ' + routerItem.ssh_user : ('routers.detailSubtitle' | translate)"
[subtitle]="subtitle"
>
<div header-actions class="header-actions-row">
<button pButton type="button" icon="pi pi-upload" [label]="'routers.exportOne' | translate" [loading]="exporting" (click)="runExport()"></button>
<button pButton type="button" severity="secondary" icon="pi pi-database" [label]="'routers.binaryOne' | translate" [loading]="runningBinary" (click)="runBinary()"></button>
<button *ngIf="!isSwitchos" pButton type="button" icon="pi pi-upload" [label]="'routers.exportOne' | translate" [loading]="exporting" (click)="runExport()"></button>
<button pButton type="button" severity="secondary" icon="pi pi-database" [label]="(isSwitchos ? 'routers.downloadSwitchBackup' : 'routers.binaryOne') | translate" [loading]="runningBinary" (click)="runBinary()"></button>
<button pButton type="button" severity="info" icon="pi pi-wifi" [label]="'routers.testConnection' | translate" [loading]="testing" (click)="testConnection()"></button>
<button pButton type="button" severity="danger" icon="pi pi-trash" [label]="'routers.deleteRouter' | translate" [loading]="deletingRouter" (click)="deleteRouter()"></button>
</div>
</app-page-header>
<div class="stats-grid compact-grid">
<app-stat-card [label]="'routers.exportsLabel' | translate" [value]="exportBackups.length" [hint]="'routers.exportsLabelHint' | translate" [tag]="'files.exportType' | translate" severity="success" icon="pi pi-file-export" iconClass="icon-emerald"></app-stat-card>
<app-stat-card [label]="'routers.deviceType' | translate" [value]="deviceTypeLabel" [hint]="'routers.listSubtitle' | translate" [tag]="'routers.fleetTag' | translate" severity="info" icon="pi pi-sitemap" iconClass="icon-blue"></app-stat-card>
<app-stat-card [label]="'routers.binaryLabel' | translate" [value]="binaryBackups.length" [hint]="'routers.binaryLabelHint' | translate" [tag]="'files.binaryType' | translate" severity="warning" icon="pi pi-database" iconClass="icon-amber"></app-stat-card>
<app-stat-card [label]="'routers.connectionLabel' | translate" [value]="connectionStateLabel" [hint]="'routers.connectionLabelHint' | translate" [tag]="'routers.probeTag' | translate" severity="info" icon="pi pi-bolt" iconClass="icon-blue"></app-stat-card>
<app-stat-card [label]="'routers.sshUser' | translate" [value]="routerItem?.ssh_user || '-'" [hint]="'routers.sshUserHint' | translate" [tag]="'routers.accessTag' | translate" severity="secondary" icon="pi pi-user" iconClass="icon-violet"></app-stat-card>
<app-stat-card [label]="'routers.sshUser' | translate" [value]="routerItem?.effective_username || '-'" [hint]="'routers.sshUserHint' | translate" [tag]="'routers.accessTag' | translate" severity="secondary" icon="pi pi-user" iconClass="icon-violet"></app-stat-card>
</div>
<div class="dashboard-grid router-detail-grid router-detail-grid--inspection">
@@ -28,6 +28,10 @@
<div class="metric-tile"><span>{{ 'routers.model' | translate }}</span><strong>{{ connection.model }}</strong></div>
<div class="metric-tile"><span>{{ 'routers.version' | translate }}</span><strong>{{ connection.version || 'n/a' }}</strong></div>
<div class="metric-tile"><span>{{ 'routers.uptime' | translate }}</span><strong>{{ connection.uptime }}</strong></div>
<div class="metric-tile" *ngIf="isSwitchos"><span>{{ 'routers.httpStatus' | translate }}</span><strong>{{ connection.http_status || '—' }}</strong></div>
<div class="metric-tile" *ngIf="isSwitchos"><span>{{ 'routers.serverHeader' | translate }}</span><strong>{{ connection.server || '—' }}</strong></div>
<div class="metric-tile" *ngIf="isSwitchos"><span>{{ 'routers.authMode' | translate }}</span><strong>{{ connection.auth_mode || '—' }}</strong></div>
<div class="metric-tile" *ngIf="isSwitchos"><span>{{ 'routers.backupEndpoint' | translate }}</span><strong>{{ connection.backup_available ? ('routers.backupAvailable' | translate) : ('routers.backupUnavailable' | translate) }}</strong></div>
</div>
<div class="router-status-error" *ngIf="!connection.success && connection.error">
<strong>{{ 'routers.lastError' | translate }}</strong>
@@ -42,7 +46,7 @@
</ng-template>
</app-section-card>
<div class="router-detail-inspection-stack">
<div class="router-detail-inspection-stack" *ngIf="!isSwitchos">
<app-section-card [title]="'routers.previewTitle' | translate" [subtitle]="'routers.previewSubtitle' | translate">
<div class="router-modal-summary" *ngIf="hasPreview; else noPreview">
<div>
@@ -81,7 +85,7 @@
</div>
</div>
<div class="dashboard-grid router-detail-grid router-detail-grid--stack">
<div class="dashboard-grid router-detail-grid router-detail-grid--stack" *ngIf="!isSwitchos">
<app-section-card [title]="'routers.exportsTableTitle' | translate" [subtitle]="'routers.exportsTableSubtitle' | translate">
<p-table [value]="exportBackups" responsiveLayout="scroll" styleClass="app-table">
<ng-template pTemplate="header">
@@ -107,7 +111,9 @@
</ng-template>
</p-table>
</app-section-card>
</div>
<div class="dashboard-grid router-detail-grid router-detail-grid--stack">
<app-section-card [title]="'routers.binaryTableTitle' | translate" [subtitle]="'routers.binaryTableSubtitle' | translate">
<p-table [value]="binaryBackups" responsiveLayout="scroll" styleClass="app-table">
<ng-template pTemplate="header">
@@ -123,7 +129,7 @@
<td>
<div class="table-actions table-actions--labels table-actions--tight">
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" icon="pi pi-download" [label]="'common.download' | translate" (click)="download(item.id)"></button>
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" severity="help" icon="pi pi-upload" [label]="'common.restore' | translate" (click)="upload(item.id)"></button>
<button *ngIf="!isSwitchos" pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" severity="help" icon="pi pi-upload" [label]="'common.restore' | translate" (click)="upload(item.id)"></button>
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" severity="secondary" icon="pi pi-envelope" [label]="'common.email' | translate" (click)="sendEmail(item.id)"></button>
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" severity="danger" icon="pi pi-trash" [label]="'common.delete' | translate" (click)="remove(item.id)"></button>
</div>

View File

@@ -14,11 +14,37 @@ 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 {
@@ -29,6 +55,11 @@ interface ConnectionSnapshot {
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 {
@@ -59,7 +90,7 @@ export class RouterDetailPageComponent implements OnInit {
private readonly ui = inject(UiService);
routerId!: number;
routerItem: any;
routerItem: DeviceItem | null = null;
backups: BackupItem[] = [];
connection: ConnectionSnapshot | null = null;
exportContent = '';
@@ -73,6 +104,10 @@ export class RouterDetailPageComponent implements OnInit {
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');
}
@@ -96,13 +131,25 @@ export class RouterDetailPageComponent implements OnInit {
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: any) => {
this.api.http.get<DeviceItem>(`${this.api.baseUrl}/routers/${this.routerId}`).subscribe((routerItem) => {
this.routerItem = routerItem;
this.connection = this.mapStoredConnection(routerItem);
});
@@ -110,7 +157,7 @@ export class RouterDetailPageComponent implements OnInit {
}
runExport() {
if (this.exporting) {
if (this.exporting || this.isSwitchos) {
return;
}
this.exporting = true;
@@ -187,6 +234,9 @@ export class RouterDetailPageComponent implements OnInit {
}
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');
});
@@ -217,7 +267,7 @@ export class RouterDetailPageComponent implements OnInit {
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.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();
@@ -241,7 +291,7 @@ export class RouterDetailPageComponent implements OnInit {
this.diffVisible = true;
}
private mapStoredConnection(routerItem: any): ConnectionSnapshot | null {
private mapStoredConnection(routerItem: DeviceItem): ConnectionSnapshot | null {
if (!routerItem?.last_connection_tested_at) {
return null;
}
@@ -252,7 +302,12 @@ export class RouterDetailPageComponent implements OnInit {
model: routerItem.last_connection_model || 'Unknown',
version: routerItem.last_connection_version,
uptime: routerItem.last_connection_uptime || 'Unknown',
error: routerItem.last_connection_error || null
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
};
}
@@ -269,6 +324,11 @@ export class RouterDetailPageComponent implements OnInit {
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
};
}

View File

@@ -11,13 +11,13 @@
</div>
<div class="inline-summary__divider"></div>
<div class="inline-summary__item">
<strong>{{ keyCount }}</strong>
<span>{{ 'routers.summaryKeyAccess' | translate }}</span>
<strong>{{ routerOsCount }}</strong>
<span>{{ 'routers.routeros' | translate }}</span>
</div>
<div class="inline-summary__divider"></div>
<div class="inline-summary__item">
<strong>{{ passwordCount }}</strong>
<span>{{ 'routers.summaryPasswordAccess' | translate }}</span>
<strong>{{ switchOsCount }}</strong>
<span>{{ 'routers.switchos' | translate }}</span>
</div>
</div>
@@ -30,16 +30,16 @@
<tr>
<td>
<div class="table-primary">{{ routerItem.name }}</div>
<small class="table-secondary">{{ 'routers.routerOsTarget' | translate }}</small>
<small class="table-secondary">{{ deviceTypeLabel(routerItem) }}</small>
</td>
<td>
<div class="table-primary">{{ routerItem.host }}:{{ routerItem.port }}</div>
<small class="table-secondary">{{ routerItem.ssh_user }}</small>
<small class="table-secondary">{{ accessUser(routerItem) }}</small>
</td>
<td>
<div class="inline-tags">
<p-tag [value]="routerItem.ssh_password ? ('routers.passwordMode' | translate) : ('routers.noPassword' | translate)" [severity]="routerItem.ssh_password ? 'warning' : 'secondary'"></p-tag>
<p-tag [value]="hasEffectiveSshKey(routerItem) ? ((usesGlobalSshKey(routerItem) ? 'routers.globalKeyMode' : 'routers.keyMode') | translate) : ('routers.noKey' | translate)" [severity]="hasEffectiveSshKey(routerItem) ? 'success' : 'secondary'"></p-tag>
<p-tag [value]="primaryAccessTag(routerItem).value" [severity]="primaryAccessTag(routerItem).severity"></p-tag>
<p-tag [value]="secondaryAccessTag(routerItem).value" [severity]="secondaryAccessTag(routerItem).severity"></p-tag>
</div>
</td>
<td>
@@ -54,33 +54,101 @@
</p-table>
</app-section-card>
<p-dialog [(visible)]="visible" [modal]="true" [header]="dialogTitle" [style]="{ width: '640px' }" styleClass="router-dialog">
<form [formGroup]="form" (ngSubmit)="save()" class="form-grid-2">
<span class="form-field">
<label>{{ 'routers.name' | translate }}</label>
<input pInputText formControlName="name" placeholder="core-router-waw" />
</span>
<span class="form-field">
<label>{{ 'routers.host' | translate }}</label>
<input pInputText formControlName="host" placeholder="10.0.0.1" />
</span>
<span class="form-field">
<label>{{ 'routers.port' | translate }}</label>
<input pInputText type="number" formControlName="port" placeholder="22" />
</span>
<span class="form-field">
<label>{{ 'routers.sshUser' | translate }}</label>
<input pInputText formControlName="ssh_user" placeholder="admin" />
</span>
<span class="form-field form-field--full">
<label>{{ 'routers.sshPassword' | translate }}</label>
<input pInputText formControlName="ssh_password" [placeholder]="'routers.optionalPassword' | translate" />
</span>
<span class="form-field form-field--full">
<label>{{ 'routers.sshPrivateKey' | translate }}</label>
<textarea pInputTextarea formControlName="ssh_key" rows="7" [placeholder]="'routers.optionalPrivateKey' | translate"></textarea>
</span>
<div class="dialog-actions">
<p-dialog [(visible)]="visible" [modal]="true" [draggable]="false" [resizable]="false" [style]="{ width: 'min(760px, 96vw)' }" styleClass="router-dialog">
<ng-template pTemplate="header">
<div class="router-dialog-header">
<div class="router-dialog-header__icon">
<i class="pi" [ngClass]="selectedDeviceType === 'switchos' ? 'pi-sitemap' : 'pi-server'"></i>
</div>
<div class="router-dialog-header__text">
<div class="router-dialog-header__eyebrow">
{{ 'routers.deviceType' | translate }} · {{ selectedDeviceType === 'switchos' ? ('routers.switchos' | translate) : ('routers.routeros' | translate) }}
</div>
<div class="router-dialog-header__title">{{ dialogTitle }}</div>
<small>
{{
selectedDeviceType === 'switchos'
? ('routers.switchDialogSubtitle' | translate)
: ('routers.routerDialogSubtitle' | translate)
}}
</small>
</div>
</div>
</ng-template>
<form [formGroup]="form" (ngSubmit)="save()" class="router-dialog-form">
<section class="router-dialog-panel">
<div class="router-dialog-panel__header">
<div>
<strong>{{ 'routers.connectionSectionTitle' | translate }}</strong>
<p>{{ 'routers.connectionSectionHint' | translate }}</p>
</div>
<span class="router-dialog-pill">
<i class="pi" [ngClass]="selectedDeviceType === 'switchos' ? 'pi-globe' : 'pi-shield'"></i>
{{ selectedDeviceType === 'switchos' ? 'HTTP' : 'SSH' }}
</span>
</div>
<div class="form-grid-2 router-dialog-grid">
<span class="form-field">
<label>{{ 'routers.name' | translate }}</label>
<input pInputText formControlName="name" placeholder="core-router-waw" />
</span>
<span class="form-field">
<label>{{ 'routers.deviceType' | translate }}</label>
<p-dropdown [options]="deviceTypeOptions" formControlName="device_type" optionLabel="label" optionValue="value"></p-dropdown>
</span>
<span class="form-field">
<label>{{ 'routers.host' | translate }}</label>
<input pInputText formControlName="host" placeholder="10.0.0.1" />
</span>
<span class="form-field">
<label>{{ 'routers.port' | translate }}</label>
<input pInputText type="number" formControlName="port" [placeholder]="selectedDeviceType === 'switchos' ? '80' : '22'" />
</span>
</div>
</section>
<section class="router-dialog-panel">
<div class="router-dialog-panel__header">
<div>
<strong>{{ 'routers.credentialsSectionTitle' | translate }}</strong>
<p>
{{
selectedDeviceType === 'switchos'
? ('routers.switchDialogSubtitle' | translate)
: ('routers.routerDialogSubtitle' | translate)
}}
</p>
</div>
<span class="router-dialog-pill">
<i class="pi pi-key"></i>
{{ selectedDeviceType === 'switchos' ? ('routers.defaultCredentials' | translate) : 'SSH' }}
</span>
</div>
<div class="form-grid-2 router-dialog-grid">
<span class="form-field">
<label>{{ 'routers.sshUser' | translate }}</label>
<input pInputText formControlName="ssh_user" [placeholder]="selectedDeviceType === 'switchos' ? ('routers.switchUserPlaceholder' | translate) : 'admin'" />
</span>
<span class="form-field">
<label>{{ 'routers.sshPassword' | translate }}</label>
<input pInputText type="password" formControlName="ssh_password" [placeholder]="selectedDeviceType === 'switchos' ? ('routers.switchPasswordPlaceholder' | translate) : ('routers.optionalPassword' | translate)" />
</span>
<span class="form-field form-field--full" *ngIf="selectedDeviceType === 'routeros'">
<label>{{ 'routers.sshPrivateKey' | translate }}</label>
<textarea pInputTextarea formControlName="ssh_key" rows="8" [placeholder]="'routers.optionalPrivateKey' | translate"></textarea>
</span>
</div>
<div class="router-dialog-note" *ngIf="selectedDeviceType === 'switchos'">
<i class="pi pi-info-circle"></i>
<span>{{ 'routers.switchDefaultsHint' | translate }}</span>
</div>
</section>
<div class="dialog-actions router-dialog-actions">
<button pButton type="button" severity="secondary" [label]="'common.cancel' | translate" (click)="visible=false"></button>
<button pButton type="submit" [disabled]="form.invalid || saving" [loading]="saving" [label]="'routers.saveRouter' | translate"></button>
</div>

View File

@@ -5,6 +5,7 @@ import { Router } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { ButtonModule } from 'primeng/button';
import { DialogModule } from 'primeng/dialog';
import { DropdownModule } from 'primeng/dropdown';
import { InputTextareaModule } from 'primeng/inputtextarea';
import { InputTextModule } from 'primeng/inputtext';
import { TableModule } from 'primeng/table';
@@ -15,21 +16,40 @@ import { UiService } from '../../core/services/ui.service';
import { PageHeaderComponent } from '../../shared/ui/page-header.component';
import { SectionCardComponent } from '../../shared/ui/section-card.component';
type DeviceType = 'routeros' | 'switchos';
interface RouterItem {
id: number;
name: string;
host: string;
port: number;
ssh_user: string;
ssh_password?: string;
ssh_key?: string;
device_type: DeviceType;
ssh_user?: string | null;
ssh_password?: string | null;
ssh_key?: string | null;
effective_username?: string | null;
uses_global_ssh_key?: boolean;
has_effective_ssh_key?: boolean;
uses_global_switchos_credentials?: boolean;
has_effective_password?: boolean;
}
@Component({
standalone: true,
imports: [CommonModule, ReactiveFormsModule, TranslateModule, ButtonModule, DialogModule, InputTextModule, InputTextareaModule, TableModule, TagModule, PageHeaderComponent, SectionCardComponent],
imports: [
CommonModule,
ReactiveFormsModule,
TranslateModule,
ButtonModule,
DialogModule,
DropdownModule,
InputTextModule,
InputTextareaModule,
TableModule,
TagModule,
PageHeaderComponent,
SectionCardComponent
],
templateUrl: './routers-page.component.html'
})
export class RoutersPageComponent implements OnInit {
@@ -42,11 +62,16 @@ export class RoutersPageComponent implements OnInit {
editingId: number | null = null;
saving = false;
routers: RouterItem[] = [];
readonly deviceTypeOptions = [
{ label: 'RouterOS', value: 'routeros' },
{ label: 'SwitchOS', value: 'switchos' }
];
readonly form = this.fb.nonNullable.group({
name: ['', Validators.required],
device_type: ['routeros' as DeviceType, Validators.required],
host: ['', Validators.required],
port: [22, Validators.required],
ssh_user: ['admin', Validators.required],
ssh_user: ['admin'],
ssh_password: '',
ssh_key: ''
});
@@ -55,24 +80,22 @@ export class RoutersPageComponent implements OnInit {
return this.ui.instant(this.editingId ? 'routers.editDialogTitle' : 'routers.createDialogTitle');
}
get passwordCount(): number {
return this.routers.filter((item) => !!item.ssh_password).length;
get selectedDeviceType(): DeviceType {
return this.form.controls.device_type.value;
}
get keyCount(): number {
return this.routers.filter((item) => this.hasEffectiveSshKey(item)).length;
get routerOsCount(): number {
return this.routers.filter((item) => item.device_type === 'routeros').length;
}
hasEffectiveSshKey(item: RouterItem): boolean {
return !!item.has_effective_ssh_key;
get switchOsCount(): number {
return this.routers.filter((item) => item.device_type === 'switchos').length;
}
usesGlobalSshKey(item: RouterItem): boolean {
return !!item.uses_global_ssh_key;
}
ngOnInit() {
this.form.controls.device_type.valueChanges.subscribe((deviceType) => {
this.applyDeviceDefaults((deviceType || 'routeros') as DeviceType);
});
this.load();
}
@@ -82,7 +105,7 @@ export class RoutersPageComponent implements OnInit {
openCreate() {
this.editingId = null;
this.form.reset({ name: '', host: '', port: 22, ssh_user: 'admin', ssh_password: '', ssh_key: '' });
this.form.reset({ name: '', device_type: 'routeros', host: '', port: 22, ssh_user: 'admin', ssh_password: '', ssh_key: '' });
this.visible = true;
}
@@ -90,9 +113,10 @@ export class RoutersPageComponent implements OnInit {
this.editingId = item.id;
this.form.reset({
name: item.name,
device_type: item.device_type,
host: item.host,
port: item.port,
ssh_user: item.ssh_user,
ssh_user: item.ssh_user ?? '',
ssh_password: item.ssh_password ?? '',
ssh_key: item.ssh_key ?? ''
});
@@ -104,9 +128,13 @@ export class RoutersPageComponent implements OnInit {
return;
}
this.saving = true;
const payload = this.form.getRawValue();
if (payload.device_type === 'switchos') {
payload.ssh_key = '';
}
const request$ = this.editingId
? this.api.http.put(`${this.api.baseUrl}/routers/${this.editingId}`, this.form.getRawValue())
: this.api.http.post(`${this.api.baseUrl}/routers`, this.form.getRawValue());
? this.api.http.put(`${this.api.baseUrl}/routers/${this.editingId}`, payload)
: this.api.http.post(`${this.api.baseUrl}/routers`, payload);
request$.subscribe({
next: () => {
@@ -134,8 +162,56 @@ export class RoutersPageComponent implements OnInit {
});
}
open(id: number) {
this.router.navigate(['/routers', id]);
}
deviceTypeLabel(item: RouterItem): string {
return this.ui.instant(item.device_type === 'switchos' ? 'routers.switchos' : 'routers.routeros');
}
accessUser(item: RouterItem): string {
return item.effective_username || item.ssh_user || '—';
}
primaryAccessTag(item: RouterItem): { value: string; severity: 'success' | 'warning' | 'secondary' | 'info' } {
if (item.device_type === 'switchos') {
if (item.uses_global_switchos_credentials) {
return { value: this.ui.instant('routers.defaultCredentials'), severity: 'info' };
}
if (item.has_effective_password) {
return { value: this.ui.instant('routers.localCredentials'), severity: 'success' };
}
return { value: this.ui.instant('routers.noCredentials'), severity: 'secondary' };
}
return {
value: item.ssh_password ? this.ui.instant('routers.passwordMode') : this.ui.instant('routers.noPassword'),
severity: item.ssh_password ? 'warning' : 'secondary'
};
}
secondaryAccessTag(item: RouterItem): { value: string; severity: 'success' | 'warning' | 'secondary' | 'info' } {
if (item.device_type === 'switchos') {
return {
value: item.has_effective_password ? this.ui.instant('routers.passwordMode') : this.ui.instant('routers.noPassword'),
severity: item.has_effective_password ? 'warning' : 'secondary'
};
}
return {
value: item.has_effective_ssh_key
? this.ui.instant(item.uses_global_ssh_key ? 'routers.globalKeyMode' : 'routers.keyMode')
: this.ui.instant('routers.noKey'),
severity: item.has_effective_ssh_key ? 'success' : 'secondary'
};
}
private applyDeviceDefaults(deviceType: DeviceType) {
if (deviceType === 'switchos') {
this.form.patchValue({ port: 80, ssh_key: '', ssh_user: this.form.controls.ssh_user.value || '' }, { emitEvent: false });
return;
}
this.form.patchValue({ port: 22, ssh_user: this.form.controls.ssh_user.value || 'admin' }, { emitEvent: false });
}
}

View File

@@ -253,7 +253,7 @@
</div>
<div class="settings-page-side">
<details class="settings-collapse settings-collapse--sticky">
<details class="settings-collapse settings-collapse--sticky" open>
<summary>
<span>{{ 'settings.sshDefaultsTitle' | translate }}</span>
<small>{{ 'settings.sshDefaultsSubtitle' | translate }}</small>
@@ -297,6 +297,25 @@
<small class="settings-ssh-note" *ngIf="clearStoredSshKey">{{ 'settings.sshKeyClearNotice' | translate }}</small>
</div>
<div class="settings-ssh-panel">
<div class="settings-ssh-panel__header">
<div>
<strong>{{ 'settings.switchosDefaultsTitle' | translate }}</strong>
<p>{{ 'settings.switchosDefaultsHint' | translate }}</p>
</div>
</div>
<div class="form-grid-2">
<span class="form-field">
<label>{{ 'settings.defaultSwitchosUsername' | translate }}</label>
<input pInputText formControlName="default_switchos_username" placeholder="admin" />
</span>
<span class="form-field">
<label>{{ 'settings.defaultSwitchosPassword' | translate }}</label>
<input pInputText formControlName="default_switchos_password" placeholder="••••••••" />
</span>
</div>
</div>
</div>
</details>
</div>

View File

@@ -55,6 +55,9 @@ interface SettingsResponse {
connection_test_interval_minutes: number;
global_ssh_key: string | null;
has_global_ssh_key: boolean;
default_switchos_username: string | null;
default_switchos_password: string | null;
has_default_switchos_credentials: boolean;
pushover_token: string | null;
pushover_userkey: string | null;
notify_failures_only: boolean;
@@ -104,6 +107,8 @@ export class SettingsPageComponent implements OnInit, OnDestroy {
enable_auto_export: false,
connection_test_interval_minutes: [0, Validators.min(0)],
global_ssh_key: '',
default_switchos_username: '',
default_switchos_password: '',
pushover_token: '',
pushover_userkey: '',
notify_failures_only: true,
@@ -376,6 +381,8 @@ export class SettingsPageComponent implements OnInit, OnDestroy {
enable_auto_export: response.enable_auto_export,
connection_test_interval_minutes: Number(response.connection_test_interval_minutes || 0),
global_ssh_key: '',
default_switchos_username: response.default_switchos_username || '',
default_switchos_password: response.default_switchos_password || '',
pushover_token: response.pushover_token || '',
pushover_userkey: response.pushover_userkey || '',
notify_failures_only: response.notify_failures_only,
@@ -404,6 +411,8 @@ export class SettingsPageComponent implements OnInit, OnDestroy {
enable_auto_export: Boolean(raw.enable_auto_export),
connection_test_interval_minutes: Number(raw.connection_test_interval_minutes || 0),
global_ssh_key: normalizedKey || null,
default_switchos_username: this.normalizeOptionalText(raw.default_switchos_username),
default_switchos_password: this.normalizeOptionalText(raw.default_switchos_password),
pushover_token: this.normalizeOptionalText(raw.pushover_token),
pushover_userkey: this.normalizeOptionalText(raw.pushover_userkey),
notify_failures_only: Boolean(raw.notify_failures_only),

View File

@@ -35,7 +35,7 @@
},
"nav": {
"dashboard": "Dashboard",
"routers": "Routers",
"routers": "Devices",
"files": "Repository",
"settings": "Settings",
"logs": "Logs",
@@ -81,7 +81,7 @@
"subtitle": "Overview of backups, exports and operational activity in one place.",
"exportAll": "Export all",
"binaryAll": "Binary backup",
"managedRouters": "Routers",
"managedRouters": "Devices",
"managedRoutersHint": "All managed devices",
"inventoryTag": "Fleet",
"exportsCard": "Exports",
@@ -134,14 +134,14 @@
"storageSnapshotHint": "Quick snapshot of the most important storage and backup indicators."
},
"routers": {
"title": "Routers",
"detailTitle": "Router details",
"add": "Add router",
"title": "Devices",
"detailTitle": "Device details",
"add": "Add device",
"eyebrow": "device inventory",
"subtitle": "Manage RouterOS endpoints, credentials and fleet-wide backup jobs.",
"subtitle": "Manage RouterOS and SwitchOS devices plus their backups.",
"registeredDevices": "Registered devices",
"fleetTag": "Fleet",
"sshPassword": "SSH password",
"sshPassword": "Password",
"passwordHint": "Password-based access",
"credsTag": "Creds",
"sshKey": "SSH key",
@@ -150,8 +150,8 @@
"defaultPort": "Port 22",
"defaultPortHint": "Standard SSH endpoints",
"portTag": "Port",
"listTitle": "Router list",
"listSubtitle": "Compact operational view of every managed device.",
"listTitle": "Device list",
"listSubtitle": "Unified view for RouterOS and SwitchOS devices.",
"name": "Name",
"endpoint": "Endpoint",
"access": "Access",
@@ -161,15 +161,15 @@
"keyMode": "Key",
"globalKeyMode": "Global key",
"noKey": "No key",
"createDialogTitle": "Add router",
"editDialogTitle": "Edit router",
"createDialogTitle": "Add device",
"editDialogTitle": "Edit device",
"host": "Host",
"port": "Port",
"sshUser": "SSH user",
"sshUser": "Username",
"sshPrivateKey": "SSH private key",
"optionalPassword": "Optional password",
"optionalPrivateKey": "Optional private key",
"saveRouter": "Save router",
"saveRouter": "Save device",
"profileEyebrow": "router profile",
"detailSubtitle": "Device operations and backup history",
"exportOne": "Export",
@@ -184,7 +184,7 @@
"connectionLabelHint": "Status from the latest automatic or manual connection test",
"probeTag": "Probe",
"accessTag": "Access",
"sshUserHint": "Current SSH user",
"sshUserHint": "Effective device login",
"deviceStatusTitle": "Device status",
"deviceStatusSubtitle": "Stored metadata from the latest automatic or manual connection test.",
"hostname": "Hostname",
@@ -200,7 +200,7 @@
"exportsTableTitle": "Exports",
"exportsTableSubtitle": "Readable RouterOS snapshots.",
"binaryTableTitle": "Binary backups",
"binaryTableSubtitle": "Files ready for device restore.",
"binaryTableSubtitle": "Binary files and SwitchOS backups.",
"summaryKeyAccess": "with key-based access",
"summaryPasswordAccess": "with password access",
"connectionStateTitle": "Connection state",
@@ -211,7 +211,28 @@
"openPreviewModal": "Open preview",
"diffModalHint": "The last loaded diff is available in a modal.",
"openDiffModal": "Open diff",
"noDiff": "Choose an export and run a diff to see the latest comparison."
"noDiff": "Choose an export and run a diff to see the latest comparison.",
"routeros": "RouterOS",
"switchos": "SwitchOS",
"deviceType": "Device type",
"defaultCredentials": "Default credentials",
"localCredentials": "Local credentials",
"noCredentials": "No credentials",
"switchUserPlaceholder": "Empty = use settings default",
"switchPasswordPlaceholder": "Empty = use settings default",
"switchDefaultsHint": "For SwitchOS you can leave username and password empty to use the defaults from settings.",
"downloadSwitchBackup": "Download backup",
"httpStatus": "HTTP status",
"serverHeader": "Server header",
"authMode": "Auth mode",
"backupEndpoint": "Backup endpoint",
"backupAvailable": "Available",
"backupUnavailable": "Unavailable",
"connectionSectionTitle": "Connection profile",
"connectionSectionHint": "Basic device identity and endpoint used to reach it.",
"credentialsSectionTitle": "Access and credentials",
"routerDialogSubtitle": "Set the device endpoint, SSH access data and your preferred login method.",
"switchDialogSubtitle": "Set the SwitchOS endpoint and optional local or shared credentials from settings."
},
"files": {
"title": "Repository",
@@ -233,14 +254,14 @@
"searchLabel": "Search",
"searchPlaceholder": "Search by file or router",
"typeLabel": "Type",
"routerLabel": "Router",
"routerLabel": "Device",
"sortLabel": "Sort by",
"orderLabel": "Order",
"allTypes": "All types",
"allRouters": "All routers",
"allRouters": "All devices",
"sortNewest": "Newest",
"sortName": "Name",
"sortRouter": "Router",
"sortRouter": "Device",
"sortType": "Type",
"tableTitle": "Repository table",
"tableSubtitle": "Artifacts available for download, e-mail and restore.",
@@ -248,7 +269,7 @@
"compareSelected": "Compare selected exports",
"fileColumn": "File",
"typeColumn": "Type",
"routerColumn": "Router",
"routerColumn": "Device",
"createdColumn": "Created",
"actionsColumn": "Actions",
"checksum": "Checksum",
@@ -311,8 +332,8 @@
"pushoverUserKey": "Pushover user key",
"pushoverTokenPlaceholder": "Application token",
"pushoverUserKeyPlaceholder": "User key",
"sshDefaultsTitle": "SSH defaults",
"sshDefaultsSubtitle": "Optional shared private key used across managed routers.",
"sshDefaultsTitle": "Default Credentials",
"sshDefaultsSubtitle": "Shared SSH key and default SwitchOS login used across managed devices.",
"globalSshPrivateKey": "Global SSH private key",
"globalSshPrivateKeyPlaceholder": "Paste PEM or OpenSSH private key",
"save": "Save settings",
@@ -377,7 +398,11 @@
"interfacePreferencesHint": "Choose the default language and font family for the whole application.",
"interfacePreferencesTag": "Per-user",
"fontFamily": "Font family",
"fontDefault": "Default"
"fontDefault": "Default",
"switchosDefaultsTitle": "Default SwitchOS credentials",
"switchosDefaultsHint": "Used when a SwitchOS device has no local username or password.",
"defaultSwitchosUsername": "Default SwitchOS username",
"defaultSwitchosPassword": "Default SwitchOS password"
},
"logs": {
"title": "Logs",

View File

@@ -35,7 +35,7 @@
},
"nav": {
"dashboard": "Panel",
"routers": "Routers",
"routers": "Dispositivos",
"files": "Repositorio",
"settings": "Ajustes",
"logs": "Registros",
@@ -81,7 +81,7 @@
"subtitle": "Resumen de copias, exportaciones y actividad operativa en un solo lugar.",
"exportAll": "Exportar todo",
"binaryAll": "Copia binaria",
"managedRouters": "Routers",
"managedRouters": "Dispositivos",
"managedRoutersHint": "Todos los dispositivos gestionados",
"inventoryTag": "Flota",
"exportsCard": "Exportaciones",
@@ -134,14 +134,14 @@
"storageSnapshotHint": "Vista rápida de los indicadores más importantes de almacenamiento y copias."
},
"routers": {
"title": "Routers",
"detailTitle": "Detalles del router",
"add": "Añadir router",
"title": "Dispositivos",
"detailTitle": "Detalles del dispositivo",
"add": "Agregar dispositivo",
"eyebrow": "inventario de dispositivos",
"subtitle": "Gestiona endpoints de RouterOS, credenciales y tareas de copia para toda la flota.",
"subtitle": "Administra dispositivos RouterOS y SwitchOS y sus copias.",
"registeredDevices": "Dispositivos registrados",
"fleetTag": "Flota",
"sshPassword": "Contraseña SSH",
"sshPassword": "Contraseña",
"passwordHint": "Acceso con contraseña",
"credsTag": "Credenciales",
"sshKey": "Clave SSH",
@@ -150,8 +150,8 @@
"defaultPort": "Puerto 22",
"defaultPortHint": "Endpoints SSH estándar",
"portTag": "Puerto",
"listTitle": "Lista de routers",
"listSubtitle": "Vista operativa compacta de todos los dispositivos gestionados.",
"listTitle": "Lista de dispositivos",
"listSubtitle": "Vista unificada para RouterOS y SwitchOS.",
"name": "Nombre",
"endpoint": "Endpoint",
"access": "Acceso",
@@ -161,15 +161,15 @@
"keyMode": "Clave",
"globalKeyMode": "Clave global",
"noKey": "Sin clave",
"createDialogTitle": "Añadir router",
"editDialogTitle": "Editar router",
"createDialogTitle": "Agregar dispositivo",
"editDialogTitle": "Editar dispositivo",
"host": "Host",
"port": "Puerto",
"sshUser": "Usuario SSH",
"sshUser": "Usuario",
"sshPrivateKey": "Clave privada SSH",
"optionalPassword": "Contraseña opcional",
"optionalPrivateKey": "Clave privada opcional",
"saveRouter": "Guardar router",
"saveRouter": "Guardar dispositivo",
"profileEyebrow": "perfil del router",
"detailSubtitle": "Operaciones del dispositivo e historial de copias",
"exportOne": "Exportar",
@@ -211,7 +211,28 @@
"openPreviewModal": "Abrir vista previa",
"diffModalHint": "El último diff cargado está disponible en un modal.",
"openDiffModal": "Abrir diff",
"noDiff": "Elige una exportación y ejecuta un diff para ver la última comparación."
"noDiff": "Elige una exportación y ejecuta un diff para ver la última comparación.",
"routeros": "RouterOS",
"switchos": "SwitchOS",
"deviceType": "Tipo de dispositivo",
"defaultCredentials": "Credenciales por defecto",
"localCredentials": "Credenciales locales",
"noCredentials": "Sin credenciales",
"switchUserPlaceholder": "Vacío = usar ajustes",
"switchPasswordPlaceholder": "Vacío = usar ajustes",
"switchDefaultsHint": "Para SwitchOS puedes dejar usuario y contraseña vacíos para usar los valores por defecto.",
"downloadSwitchBackup": "Descargar copia",
"httpStatus": "Estado HTTP",
"serverHeader": "Cabecera Server",
"authMode": "Modo de autenticación",
"backupEndpoint": "Endpoint de copia",
"backupAvailable": "Disponible",
"backupUnavailable": "No disponible",
"connectionSectionTitle": "Perfil de conexión",
"connectionSectionHint": "Identidad básica del dispositivo y endpoint usado para alcanzarlo.",
"credentialsSectionTitle": "Acceso y credenciales",
"routerDialogSubtitle": "Configura el endpoint del dispositivo, los datos SSH y el método de acceso preferido.",
"switchDialogSubtitle": "Configura el endpoint de SwitchOS y las credenciales locales u opcionales compartidas desde ajustes."
},
"files": {
"title": "Repositorio",
@@ -233,14 +254,14 @@
"searchLabel": "Buscar",
"searchPlaceholder": "Buscar por archivo o router",
"typeLabel": "Tipo",
"routerLabel": "Router",
"routerLabel": "Dispositivo",
"sortLabel": "Ordenar por",
"orderLabel": "Orden",
"allTypes": "Todos los tipos",
"allRouters": "Todos los routers",
"allRouters": "Todos los dispositivos",
"sortNewest": "Más nuevo",
"sortName": "Nombre",
"sortRouter": "Router",
"sortRouter": "Dispositivo",
"sortType": "Tipo",
"tableTitle": "Tabla del repositorio",
"tableSubtitle": "Artefactos disponibles para descarga, correo y restauración.",
@@ -248,7 +269,7 @@
"compareSelected": "Comparar exportaciones seleccionadas",
"fileColumn": "Archivo",
"typeColumn": "Tipo",
"routerColumn": "Router",
"routerColumn": "Dispositivo",
"createdColumn": "Creado",
"actionsColumn": "Acciones",
"checksum": "Checksum",
@@ -311,8 +332,8 @@
"pushoverUserKey": "Clave de usuario de Pushover",
"pushoverTokenPlaceholder": "Token de la aplicación",
"pushoverUserKeyPlaceholder": "Clave de usuario",
"sshDefaultsTitle": "Valores SSH por defecto",
"sshDefaultsSubtitle": "Clave privada compartida opcional usada en todos los routers gestionados.",
"sshDefaultsTitle": "Credenciales predeterminadas",
"sshDefaultsSubtitle": "Clave SSH compartida y acceso por defecto de SwitchOS usados por los dispositivos gestionados.",
"globalSshPrivateKey": "Clave privada SSH global",
"globalSshPrivateKeyPlaceholder": "Pega la clave privada PEM u OpenSSH",
"globalSshPrivateKeyHiddenPlaceholder": "La clave guardada está oculta. Introduce la contraseña arriba para verla o pega aquí una nueva clave para reemplazarla.",
@@ -377,7 +398,11 @@
"interfacePreferencesHint": "Elige el idioma predeterminado y la familia tipográfica para toda la aplicación.",
"interfacePreferencesTag": "Por usuario",
"fontFamily": "Familia tipográfica",
"fontDefault": "Predeterminada"
"fontDefault": "Predeterminada",
"switchosDefaultsTitle": "Credenciales por defecto de SwitchOS",
"switchosDefaultsHint": "Se usan cuando un dispositivo SwitchOS no tiene usuario o contraseña local.",
"defaultSwitchosUsername": "Usuario SwitchOS por defecto",
"defaultSwitchosPassword": "Contraseña SwitchOS por defecto"
},
"logs": {
"title": "Registros",

View File

@@ -35,7 +35,7 @@
},
"nav": {
"dashboard": "Dashbord",
"routers": "Rutere",
"routers": "Enheter",
"files": "Repository",
"settings": "Innstillinger",
"logs": "Logger",
@@ -81,7 +81,7 @@
"subtitle": "Oversikt over backuper, eksportfiler og operativ aktivitet på ett sted.",
"exportAll": "Eksporter alle",
"binaryAll": "Binær backup",
"managedRouters": "Rutere",
"managedRouters": "Enheter",
"managedRoutersHint": "Alle administrerte enheter",
"inventoryTag": "Flåte",
"exportsCard": "Eksporter",
@@ -134,14 +134,14 @@
"storageSnapshotHint": "Rask oversikt over de viktigste lagrings- og backupindikatorene."
},
"routers": {
"title": "Rutere",
"detailTitle": "Ruterdetaljer",
"add": "Legg til ruter",
"title": "Enheter",
"detailTitle": "Enhetsdetaljer",
"add": "Legg til enhet",
"eyebrow": "enhetsinventar",
"subtitle": "Administrer RouterOS-endepunkter, legitimasjon og backupjobber for hele flåten.",
"subtitle": "Administrer RouterOS- og SwitchOS-enheter og sikkerhetskopier.",
"registeredDevices": "Registrerte enheter",
"fleetTag": "Flåte",
"sshPassword": "SSH-passord",
"sshPassword": "Passord",
"passwordHint": "Passordbasert tilgang",
"credsTag": "Tilgang",
"sshKey": "SSH-nøkkel",
@@ -150,8 +150,8 @@
"defaultPort": "Port 22",
"defaultPortHint": "Standard SSH-endepunkter",
"portTag": "Port",
"listTitle": "Ruterliste",
"listSubtitle": "Kompakt driftsvisning av alle administrerte enheter.",
"listTitle": "Enhetsliste",
"listSubtitle": "Felles visning for RouterOS og SwitchOS.",
"name": "Navn",
"endpoint": "Endepunkt",
"access": "Tilgang",
@@ -161,15 +161,15 @@
"keyMode": "Nøkkel",
"globalKeyMode": "Global nøkkel",
"noKey": "Ingen nøkkel",
"createDialogTitle": "Legg til ruter",
"editDialogTitle": "Rediger ruter",
"createDialogTitle": "Legg til enhet",
"editDialogTitle": "Rediger enhet",
"host": "Vert",
"port": "Port",
"sshUser": "SSH-bruker",
"sshUser": "Bruker",
"sshPrivateKey": "SSH privat nøkkel",
"optionalPassword": "Valgfritt passord",
"optionalPrivateKey": "Valgfri privat nøkkel",
"saveRouter": "Lagre ruter",
"saveRouter": "Lagre enhet",
"profileEyebrow": "ruterprofil",
"detailSubtitle": "Enhetsoperasjoner og backuphistorikk",
"exportOne": "Eksport",
@@ -211,7 +211,28 @@
"openPreviewModal": "Åpne forhåndsvisning",
"diffModalHint": "Sist lastede diff er tilgjengelig i en modal.",
"openDiffModal": "Åpne diff",
"noDiff": "Velg en eksport og kjør diff for å se siste sammenligning."
"noDiff": "Velg en eksport og kjør diff for å se siste sammenligning.",
"routeros": "RouterOS",
"switchos": "SwitchOS",
"deviceType": "Enhetstype",
"defaultCredentials": "Standard legitimasjon",
"localCredentials": "Lokal legitimasjon",
"noCredentials": "Ingen legitimasjon",
"switchUserPlaceholder": "Tom = bruk innstillinger",
"switchPasswordPlaceholder": "Tom = bruk innstillinger",
"switchDefaultsHint": "For SwitchOS kan du la bruker og passord være tomme for å bruke standardverdier fra innstillinger.",
"downloadSwitchBackup": "Last ned backup",
"httpStatus": "HTTP-status",
"serverHeader": "Server-header",
"authMode": "Autentiseringsmodus",
"backupEndpoint": "Backup-endepunkt",
"backupAvailable": "Tilgjengelig",
"backupUnavailable": "Utilgjengelig",
"connectionSectionTitle": "Tilkoblingsprofil",
"connectionSectionHint": "Grunnleggende enhetsidentitet og endpoint som brukes for å nå den.",
"credentialsSectionTitle": "Tilgang og legitimasjon",
"routerDialogSubtitle": "Sett enhetens endpoint, SSH-data og foretrukket innloggingsmetode.",
"switchDialogSubtitle": "Sett SwitchOS-endpoint og valgfrie lokale eller delte standarddata fra innstillinger."
},
"files": {
"title": "Repository",
@@ -233,14 +254,14 @@
"searchLabel": "Søk",
"searchPlaceholder": "Søk etter fil eller ruter",
"typeLabel": "Type",
"routerLabel": "Ruter",
"routerLabel": "Enhet",
"sortLabel": "Sorter etter",
"orderLabel": "Rekkefølge",
"allTypes": "Alle typer",
"allRouters": "Alle rutere",
"allRouters": "Alle enheter",
"sortNewest": "Nyeste",
"sortName": "Navn",
"sortRouter": "Ruter",
"sortRouter": "Enhet",
"sortType": "Type",
"tableTitle": "Repositorytabell",
"tableSubtitle": "Artefakter tilgjengelige for nedlasting, e-post og gjenoppretting.",
@@ -248,7 +269,7 @@
"compareSelected": "Sammenlign valgte eksporter",
"fileColumn": "Fil",
"typeColumn": "Type",
"routerColumn": "Ruter",
"routerColumn": "Enhet",
"createdColumn": "Opprettet",
"actionsColumn": "Handlinger",
"checksum": "Checksum",
@@ -311,8 +332,8 @@
"pushoverUserKey": "Pushover-brukernøkkel",
"pushoverTokenPlaceholder": "Applikasjonstoken",
"pushoverUserKeyPlaceholder": "Brukernøkkel",
"sshDefaultsTitle": "SSH-standarder",
"sshDefaultsSubtitle": "Valgfri delt privat nøkkel som brukes tvers av administrerte rutere.",
"sshDefaultsTitle": "Standard legitimasjon",
"sshDefaultsSubtitle": "Delt SSH-nøkkel og standard innlogging for SwitchOS brukt på administrerte enheter.",
"globalSshPrivateKey": "Global SSH privat nøkkel",
"globalSshPrivateKeyPlaceholder": "Lim inn PEM- eller OpenSSH-privat nøkkel",
"globalSshPrivateKeyHiddenPlaceholder": "Den lagrede nøkkelen er skjult. Skriv inn passordet over for å se den, eller lim inn en ny nøkkel her for å erstatte den.",
@@ -377,7 +398,11 @@
"interfacePreferencesHint": "Velg standardspråk og skriftfamilie for hele applikasjonen.",
"interfacePreferencesTag": "Per bruker",
"fontFamily": "Skriftfamilie",
"fontDefault": "Standard"
"fontDefault": "Standard",
"switchosDefaultsTitle": "Standard SwitchOS-legitimasjon",
"switchosDefaultsHint": "Brukes når en SwitchOS-enhet ikke har lokalt brukernavn eller passord.",
"defaultSwitchosUsername": "Standard SwitchOS-bruker",
"defaultSwitchosPassword": "Standard SwitchOS-passord"
},
"logs": {
"title": "Logger",

View File

@@ -35,7 +35,7 @@
},
"nav": {
"dashboard": "Dashboard",
"routers": "Routery",
"routers": "Urządzenia",
"files": "Repozytorium",
"settings": "Ustawienia",
"logs": "Logi",
@@ -81,7 +81,7 @@
"subtitle": "Przegląd backupów, eksportów i aktywności operacyjnej w jednym miejscu.",
"exportAll": "Eksportuj wszystko",
"binaryAll": "Backup binarny",
"managedRouters": "Routery",
"managedRouters": "Urządzenia",
"managedRoutersHint": "Wszystkie zarządzane urządzenia",
"inventoryTag": "Flota",
"exportsCard": "Eksporty",
@@ -134,14 +134,14 @@
"storageSnapshotHint": "Szybki podgląd najważniejszych wskaźników przestrzeni i backupów."
},
"routers": {
"title": "Routery",
"detailTitle": "Szczegóły routera",
"add": "Dodaj router",
"title": "Urządzenia",
"detailTitle": "Szczegóły urządzenia",
"add": "Dodaj urządzenie",
"eyebrow": "inwentaryzacja urządzeń",
"subtitle": "Zarządzaj endpointami RouterOS, poświadczeniami i zadaniami backupu dla całej floty.",
"subtitle": "Zarządzaj urządzeniami RouterOS i SwitchOS oraz ich kopiami.",
"registeredDevices": "Zarejestrowane urządzenia",
"fleetTag": "Flota",
"sshPassword": "Hasło SSH",
"sshPassword": "Hasło",
"passwordHint": "Dostęp hasłem",
"credsTag": "Dostęp",
"sshKey": "Klucz SSH",
@@ -150,8 +150,8 @@
"defaultPort": "Port 22",
"defaultPortHint": "Standardowe endpointy SSH",
"portTag": "Port",
"listTitle": "Lista routerów",
"listSubtitle": "Zwięzły widok operacyjny wszystkich zarządzanych urządzeń.",
"listTitle": "Lista urządzeń",
"listSubtitle": "Wspólny widok RouterOS i SwitchOS.",
"name": "Nazwa",
"endpoint": "Endpoint",
"access": "Dostęp",
@@ -161,15 +161,15 @@
"keyMode": "Klucz",
"globalKeyMode": "Klucz globalny",
"noKey": "Bez klucza",
"createDialogTitle": "Dodaj router",
"editDialogTitle": "Edytuj router",
"createDialogTitle": "Dodaj urządzenie",
"editDialogTitle": "Edytuj urządzenie",
"host": "Host",
"port": "Port",
"sshUser": "Użytkownik SSH",
"sshUser": "Użytkownik",
"sshPrivateKey": "Klucz prywatny SSH",
"optionalPassword": "Opcjonalne hasło",
"optionalPrivateKey": "Opcjonalny klucz prywatny",
"saveRouter": "Zapisz router",
"saveRouter": "Zapisz urządzenie",
"profileEyebrow": "profil routera",
"detailSubtitle": "Operacje urządzenia i historia backupów",
"exportOne": "Eksport",
@@ -184,7 +184,7 @@
"connectionLabelHint": "Status z ostatniego automatycznego lub ręcznego testu połączenia",
"probeTag": "Test",
"accessTag": "Dostęp",
"sshUserHint": "Bieżący użytkownik SSH",
"sshUserHint": "Efektywny login urządzenia",
"deviceStatusTitle": "Status urządzenia",
"deviceStatusSubtitle": "Zapisane metadane z ostatniego automatycznego lub ręcznego testu połączenia.",
"hostname": "Hostname",
@@ -200,7 +200,7 @@
"exportsTableTitle": "Eksporty",
"exportsTableSubtitle": "Czytelne snapshoty RouterOS.",
"binaryTableTitle": "Backupy binarne",
"binaryTableSubtitle": "Pliki do odtworzenia urządzenia.",
"binaryTableSubtitle": "Pliki binarne i kopie SwitchOS.",
"summaryKeyAccess": "z dostępem kluczem",
"summaryPasswordAccess": "z dostępem hasłem",
"connectionStateTitle": "Stan połączenia",
@@ -211,7 +211,28 @@
"openPreviewModal": "Otwórz podgląd",
"diffModalHint": "Ostatnio załadowany diff jest dostępny w modalu.",
"openDiffModal": "Otwórz diff",
"noDiff": "Wybierz eksport i uruchom diff, aby zobaczyć ostatnie porównanie."
"noDiff": "Wybierz eksport i uruchom diff, aby zobaczyć ostatnie porównanie.",
"routeros": "RouterOS",
"switchos": "SwitchOS",
"deviceType": "Typ urządzenia",
"defaultCredentials": "Domyślne dane",
"localCredentials": "Lokalne dane",
"noCredentials": "Brak danych",
"switchUserPlaceholder": "Puste = z ustawień",
"switchPasswordPlaceholder": "Puste = z ustawień",
"switchDefaultsHint": "Dla SwitchOS możesz zostawić użytkownika i hasło puste, aby użyć wartości domyślnych z ustawień.",
"downloadSwitchBackup": "Pobierz backup",
"httpStatus": "Status HTTP",
"serverHeader": "Nagłówek Server",
"authMode": "Tryb autoryzacji",
"backupEndpoint": "Endpoint backupu",
"backupAvailable": "Dostępny",
"backupUnavailable": "Niedostępny",
"connectionSectionTitle": "Profil połączenia",
"connectionSectionHint": "Podstawowa tożsamość urządzenia i endpoint używany do połączenia.",
"credentialsSectionTitle": "Dostęp i poświadczenia",
"routerDialogSubtitle": "Ustaw adres urządzenia, dane dostępu SSH i preferowaną metodę logowania.",
"switchDialogSubtitle": "Ustaw endpoint SwitchOS i opcjonalne poświadczenia lokalne lub domyślne z ustawień."
},
"files": {
"title": "Repozytorium",
@@ -233,14 +254,14 @@
"searchLabel": "Szukaj",
"searchPlaceholder": "Szukaj po pliku lub routerze",
"typeLabel": "Typ",
"routerLabel": "Router",
"routerLabel": "Urządzenie",
"sortLabel": "Sortowanie",
"orderLabel": "Kolejność",
"allTypes": "Wszystkie typy",
"allRouters": "Wszystkie routery",
"allRouters": "Wszystkie urządzenia",
"sortNewest": "Najnowsze",
"sortName": "Nazwa",
"sortRouter": "Router",
"sortRouter": "Urządzenie",
"sortType": "Typ",
"tableTitle": "Tabela repozytorium",
"tableSubtitle": "Artefakty dostępne do pobrania, wysyłki e-mail i przywracania.",
@@ -248,7 +269,7 @@
"compareSelected": "Porównaj zaznaczone eksporty",
"fileColumn": "Plik",
"typeColumn": "Typ",
"routerColumn": "Router",
"routerColumn": "Urządzenie",
"createdColumn": "Utworzono",
"actionsColumn": "Akcje",
"checksum": "Checksum",
@@ -311,8 +332,8 @@
"pushoverUserKey": "Klucz użytkownika Pushover",
"pushoverTokenPlaceholder": "Token aplikacji",
"pushoverUserKeyPlaceholder": "Klucz użytkownika",
"sshDefaultsTitle": "Domyślne SSH",
"sshDefaultsSubtitle": "Opcjonalny współdzielony klucz prywatny używany przez zarządzane routery.",
"sshDefaultsTitle": "Domyślne Poświadczenia",
"sshDefaultsSubtitle": "Wspólny klucz SSH oraz domyślne logowanie SwitchOS używane przez urządzenia.",
"globalSshPrivateKey": "Globalny klucz prywatny SSH",
"globalSshPrivateKeyPlaceholder": "Wklej klucz prywatny PEM lub OpenSSH",
"save": "Zapisz ustawienia",
@@ -377,7 +398,11 @@
"interfacePreferencesHint": "Wybierz domyślny język i rodzinę fontów dla całej aplikacji.",
"interfacePreferencesTag": "Per-user",
"fontFamily": "Rodzina fontów",
"fontDefault": "Domyślna"
"fontDefault": "Domyślna",
"switchosDefaultsTitle": "Domyślne dane SwitchOS",
"switchosDefaultsHint": "Używane, gdy urządzenie SwitchOS nie ma własnego loginu lub hasła.",
"defaultSwitchosUsername": "Domyślny użytkownik SwitchOS",
"defaultSwitchosPassword": "Domyślne hasło SwitchOS"
},
"logs": {
"title": "Logi",

View File

@@ -3389,3 +3389,198 @@ body.dark-theme .p-confirm-dialog .p-confirm-dialog-icon{
@media (max-width: 991px) {
}
.router-dialog .p-dialog-header{
padding: 1.15rem 1.2rem 1rem;
align-items: flex-start;
background:
linear-gradient(135deg, rgba(75, 144, 217, 0.16), rgba(79, 181, 147, 0.1)),
linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0)),
var(--surface-1);
border-bottom: 1px solid rgba(75, 144, 217, 0.18);
}
.router-dialog .p-dialog-header-icons{
align-self: flex-start;
margin-top: 0.2rem;
}
.router-dialog .p-dialog-content{
padding: 0 1.2rem 1.2rem;
background: linear-gradient(180deg, rgba(75, 144, 217, 0.06) 0%, rgba(75, 144, 217, 0) 180px), var(--surface-1);
}
.router-dialog-header{
display: flex;
align-items: center;
gap: 0.9rem;
width: calc(100% - 0.5rem);
}
.router-dialog-header__icon{
width: 3rem;
height: 3rem;
border-radius: 18px;
display: grid;
place-items: center;
flex-shrink: 0;
background: linear-gradient(135deg, rgba(75, 144, 217, 0.24), rgba(79, 181, 147, 0.14));
border: 1px solid rgba(75, 144, 217, 0.2);
color: var(--primary);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
.router-dialog-header__icon .pi{
font-size: 1.05rem;
}
.router-dialog-header__text{
min-width: 0;
}
.router-dialog-header__eyebrow{
font-family: var(--font-title);
font-size: 0.72rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--text-soft);
}
.router-dialog-header__title{
margin-top: 0.2rem;
font-family: var(--font-title);
font-size: 1.18rem;
line-height: 1.25;
}
.router-dialog-header__text small{
display: block;
margin-top: 0.3rem;
max-width: 42rem;
line-height: 1.55;
}
.router-dialog-form{
display: grid;
gap: 1rem;
}
.router-dialog-panel{
padding: 1rem;
border-radius: 22px;
border: 1px solid var(--border-color);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0)), var(--surface-0);
box-shadow: var(--shadow-md);
}
.router-dialog-panel:first-child{
margin-top: 1rem;
}
.router-dialog-panel__header{
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1rem;
margin-bottom: 0.95rem;
}
.router-dialog-panel__header strong{
display: block;
font-family: var(--font-title);
font-size: 0.88rem;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.router-dialog-panel__header p{
margin: 0.35rem 0 0;
color: var(--text-soft);
line-height: 1.55;
}
.router-dialog-pill{
display: inline-flex;
align-items: center;
gap: 0.45rem;
padding: 0.55rem 0.85rem;
border-radius: 999px;
border: 1px solid rgba(75, 144, 217, 0.18);
background: rgba(75, 144, 217, 0.08);
color: var(--text-main);
font-family: var(--font-title);
font-size: 0.72rem;
letter-spacing: 0.08em;
text-transform: uppercase;
white-space: nowrap;
}
.router-dialog-grid{
gap: 0.95rem 1rem;
}
.router-dialog-note{
margin-top: 0.9rem;
padding: 0.85rem 0.95rem;
border-radius: 16px;
border: 1px solid rgba(75, 144, 217, 0.18);
background: rgba(75, 144, 217, 0.08);
color: var(--text-soft);
display: flex;
align-items: flex-start;
gap: 0.65rem;
line-height: 1.55;
}
.router-dialog-note .pi{
margin-top: 0.1rem;
color: var(--accent);
}
.router-dialog .p-inputtext,
.router-dialog .p-dropdown,
.router-dialog .p-inputtextarea{
background: rgba(255, 255, 255, 0.02);
}
.router-dialog .p-inputtextarea{
min-height: 11rem;
}
.router-dialog-actions{
justify-content: space-between;
gap: 0.75rem;
padding-top: 0.1rem;
}
.router-dialog-actions .p-button{
min-width: 11rem;
}
@media (max-width: 720px) {
.router-dialog .p-dialog-header{
padding: 1rem 0.85rem 0.85rem;
}
.router-dialog .p-dialog-content{
padding: 0 0.85rem 0.95rem;
}
.router-dialog-header{
align-items: flex-start;
}
.router-dialog-panel__header{
flex-direction: column;
}
.router-dialog-actions{
flex-direction: column-reverse;
align-items: stretch;
}
.router-dialog-actions .p-button{
width: 100%;
min-width: 0;
}
}