first commit
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -33,3 +33,4 @@ frontend/.cache/
|
||||
docker-data/db/*
|
||||
docker-data/*.rsc
|
||||
docker-data/*.backup
|
||||
storage/*
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# RouterOS Backup Manager Next
|
||||
# Mikrotik Backup System
|
||||
|
||||
## Deploy in docker
|
||||
```bash
|
||||
|
||||
@@ -5,7 +5,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
app_name: str = 'RouterOS Backup Manager Next'
|
||||
app_name: str = 'Mikrotik Backup System'
|
||||
app_env: str = 'development'
|
||||
secret_key: str = 'change-me'
|
||||
jwt_algorithm: str = 'HS256'
|
||||
|
||||
@@ -306,8 +306,12 @@ class BackupService:
|
||||
routers = db.query(Router).filter(Router.owner_id == user.id).all()
|
||||
result = []
|
||||
for router in routers:
|
||||
if router.device_type != 'routeros':
|
||||
result.append({'router': router.name, 'status': 'skipped', 'message': 'SwitchOS devices do not support text export'})
|
||||
if (router.device_type or 'routeros').lower() != 'routeros':
|
||||
result.append({
|
||||
'router': router.name,
|
||||
'status': 'skipped',
|
||||
'message': 'Text export is available only for RouterOS devices',
|
||||
})
|
||||
continue
|
||||
try:
|
||||
backup = self.export_router(db, user, router.id)
|
||||
|
||||
@@ -53,7 +53,7 @@ class NotificationService:
|
||||
return
|
||||
if settings.smtp_notifications_enabled:
|
||||
try:
|
||||
self.send_email(settings, "RouterOS Backup notification", message)
|
||||
self.send_email(settings, "Mikrotik Backup System notification", message)
|
||||
except Exception:
|
||||
pass
|
||||
if settings.pushover_token and settings.pushover_userkey:
|
||||
@@ -63,7 +63,7 @@ class NotificationService:
|
||||
pass
|
||||
|
||||
def send_test_email(self, settings: GlobalSettings):
|
||||
self.send_email(settings, "RouterOS Backup test", "This is a test email from RouterOS Backup Manager Next")
|
||||
self.send_email(settings, "Mikrotik Backup System test", "This is a test email from Mikrotik Backup System")
|
||||
|
||||
def send_test_pushover(self, settings: GlobalSettings):
|
||||
if not (settings.pushover_token and settings.pushover_userkey):
|
||||
@@ -71,7 +71,7 @@ class NotificationService:
|
||||
self.send_pushover(
|
||||
settings.pushover_token,
|
||||
settings.pushover_userkey,
|
||||
"Test pushover from RouterOS Backup Manager Next",
|
||||
"Test pushover from Mikrotik Backup System",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.main import app
|
||||
|
||||
|
||||
def _login(client: TestClient) -> tuple[str, dict[str, str]]:
|
||||
response = client.post('/api/auth/login', data={'username': 'admin', 'password': 'admin'})
|
||||
token = response.json()['access_token']
|
||||
return token, {'Authorization': f'Bearer {token}'}
|
||||
|
||||
|
||||
def test_switchos_list_marks_global_credentials_usage(monkeypatch, tmp_path):
|
||||
from app.api.routes import settings as settings_route
|
||||
|
||||
with TestClient(app) as client:
|
||||
_, headers = _login(client)
|
||||
settings_response = client.put(
|
||||
'/api/settings',
|
||||
json={
|
||||
'backup_retention_days': 7,
|
||||
'log_retention_days': 7,
|
||||
'export_cron': '',
|
||||
'binary_cron': '',
|
||||
'retention_cron': '',
|
||||
'enable_auto_export': False,
|
||||
'connection_test_interval_minutes': 0,
|
||||
'global_ssh_key': None,
|
||||
'default_switchos_username': 'sw-admin',
|
||||
'default_switchos_password': 'sw-pass',
|
||||
'pushover_token': None,
|
||||
'pushover_userkey': None,
|
||||
'notify_failures_only': True,
|
||||
'smtp_host': None,
|
||||
'smtp_port': 587,
|
||||
'smtp_login': None,
|
||||
'smtp_password': None,
|
||||
'smtp_notifications_enabled': False,
|
||||
'recipient_email': None,
|
||||
'clear_global_ssh_key': False,
|
||||
},
|
||||
headers=headers,
|
||||
)
|
||||
assert settings_response.status_code == 200
|
||||
assert settings_response.json()['has_default_switchos_credentials'] is True
|
||||
|
||||
create_response = client.post(
|
||||
'/api/routers',
|
||||
json={
|
||||
'name': 'switch01',
|
||||
'device_type': 'switchos',
|
||||
'host': '192.168.88.2',
|
||||
'port': 80,
|
||||
'ssh_user': '',
|
||||
'ssh_password': '',
|
||||
'ssh_key': None,
|
||||
},
|
||||
headers=headers,
|
||||
)
|
||||
assert create_response.status_code == 200
|
||||
|
||||
list_response = client.get('/api/routers', headers=headers)
|
||||
assert list_response.status_code == 200
|
||||
payload = next(item for item in list_response.json() if item['name'] == 'switch01')
|
||||
assert payload['device_type'] == 'switchos'
|
||||
assert payload['uses_global_switchos_credentials'] is True
|
||||
assert payload['effective_username'] == 'sw-admin'
|
||||
|
||||
|
||||
def test_switchos_connection_probe_is_exposed_in_device_route(monkeypatch):
|
||||
from app.api.routes import routers as routers_route
|
||||
|
||||
with TestClient(app) as client:
|
||||
_, headers = _login(client)
|
||||
create_response = client.post(
|
||||
'/api/routers',
|
||||
json={
|
||||
'name': 'switch02',
|
||||
'device_type': 'switchos',
|
||||
'host': '192.168.88.3',
|
||||
'port': 80,
|
||||
'ssh_user': 'admin',
|
||||
'ssh_password': 'secret',
|
||||
'ssh_key': None,
|
||||
},
|
||||
headers=headers,
|
||||
)
|
||||
device_id = create_response.json()['id']
|
||||
|
||||
monkeypatch.setattr(
|
||||
routers_route.router_service,
|
||||
'test_connection',
|
||||
lambda db, router, global_settings: {
|
||||
'success': True,
|
||||
'tested_at': datetime(2026, 4, 13, 10, 0, 0),
|
||||
'model': 'SwitchOS',
|
||||
'uptime': 'HTTP 200',
|
||||
'hostname': 'MikroTik SwitchOS',
|
||||
'version': None,
|
||||
'error': None,
|
||||
'transport': 'http',
|
||||
'server': 'MikroTik',
|
||||
'auth_mode': 'digest',
|
||||
'http_status': '200',
|
||||
'backup_available': True,
|
||||
},
|
||||
)
|
||||
|
||||
response = client.get(f'/api/routers/{device_id}/test-connection', headers=headers)
|
||||
assert response.status_code == 200
|
||||
assert response.json()['transport'] == 'http'
|
||||
assert response.json()['backup_available'] is True
|
||||
|
||||
|
||||
def test_switchos_binary_backup_is_saved_as_swb(monkeypatch, tmp_path):
|
||||
from app.services import backup_service as backup_service_module
|
||||
from app.services import router_service as router_service_module
|
||||
|
||||
data_dir = tmp_path / 'data'
|
||||
data_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
monkeypatch.setattr(backup_service_module, 'ensure_data_dir', lambda: data_dir)
|
||||
|
||||
def fake_binary_backup(router, backup_name, local_path, global_ssh_key=None, global_settings=None):
|
||||
Path(local_path).write_bytes(b'switchos-binary')
|
||||
return local_path
|
||||
|
||||
monkeypatch.setattr(router_service_module.router_service, 'binary_backup', fake_binary_backup)
|
||||
|
||||
with TestClient(app) as client:
|
||||
_, headers = _login(client)
|
||||
create_response = client.post(
|
||||
'/api/routers',
|
||||
json={
|
||||
'name': 'switch03',
|
||||
'device_type': 'switchos',
|
||||
'host': '192.168.88.4',
|
||||
'port': 80,
|
||||
'ssh_user': 'admin',
|
||||
'ssh_password': 'secret',
|
||||
'ssh_key': None,
|
||||
},
|
||||
headers=headers,
|
||||
)
|
||||
device_id = create_response.json()['id']
|
||||
|
||||
backup_response = client.post(f'/api/backups/router/{device_id}/binary', headers=headers)
|
||||
assert backup_response.status_code == 200
|
||||
assert backup_response.json()['backup_type'] == 'binary'
|
||||
assert backup_response.json()['file_name'].endswith('.swb')
|
||||
|
||||
logs_response = client.get('/api/logs', headers=headers)
|
||||
assert logs_response.status_code == 200
|
||||
assert any('Binary backup OK for SwitchOS device switch03' in item['message'] for item in logs_response.json())
|
||||
|
||||
|
||||
def test_switchos_connection_test_creates_operation_log(monkeypatch):
|
||||
from app.services import router_service as router_service_module
|
||||
|
||||
monkeypatch.setattr(
|
||||
router_service_module.router_service,
|
||||
'probe_connection',
|
||||
lambda router, global_ssh_key=None, global_settings=None: {
|
||||
'success': True,
|
||||
'tested_at': datetime(2026, 4, 13, 10, 0, 0),
|
||||
'model': 'SwitchOS',
|
||||
'uptime': 'HTTP 200',
|
||||
'hostname': 'switch04',
|
||||
'version': None,
|
||||
'error': None,
|
||||
'transport': 'http',
|
||||
'server': 'MikroTik',
|
||||
'auth_mode': 'digest',
|
||||
'http_status': '200',
|
||||
'backup_available': True,
|
||||
},
|
||||
)
|
||||
|
||||
with TestClient(app) as client:
|
||||
_, headers = _login(client)
|
||||
create_response = client.post(
|
||||
'/api/routers',
|
||||
json={
|
||||
'name': 'switch04',
|
||||
'device_type': 'switchos',
|
||||
'host': '192.168.88.5',
|
||||
'port': 80,
|
||||
'ssh_user': 'admin',
|
||||
'ssh_password': 'secret',
|
||||
'ssh_key': None,
|
||||
},
|
||||
headers=headers,
|
||||
)
|
||||
assert create_response.status_code == 200
|
||||
device_id = create_response.json()['id']
|
||||
|
||||
response = client.get(f'/api/routers/{device_id}/test-connection', headers=headers)
|
||||
assert response.status_code == 200
|
||||
|
||||
logs_response = client.get('/api/logs', headers=headers)
|
||||
assert logs_response.status_code == 200
|
||||
assert any(
|
||||
'Connection test OK for SwitchOS device switch04' in item['message']
|
||||
and 'auth=digest' in item['message']
|
||||
and 'http=200' in item['message']
|
||||
for item in logs_response.json()
|
||||
)
|
||||
@@ -2,7 +2,7 @@
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"projects": {
|
||||
"routeros-backup-manager-next-ui": {
|
||||
"mikrotik-backup-system-ui": {
|
||||
"projectType": "application",
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
@@ -11,7 +11,7 @@
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"outputPath": "dist/routeros-backup-manager-next-ui",
|
||||
"outputPath": "dist/mikrotik-backup-system-ui",
|
||||
"index": "src/index.html",
|
||||
"browser": "src/main.ts",
|
||||
"polyfills": [
|
||||
@@ -24,8 +24,6 @@
|
||||
],
|
||||
"styles": [
|
||||
"node_modules/primeicons/primeicons.css",
|
||||
"node_modules/primeng/resources/themes/lara-light-blue/theme.css",
|
||||
"node_modules/primeng/resources/primeng.min.css",
|
||||
"src/styles.css"
|
||||
]
|
||||
},
|
||||
@@ -38,8 +36,8 @@
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "1mb",
|
||||
"maximumError": "2mb"
|
||||
"maximumWarning": "2mb",
|
||||
"maximumError": "3mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
@@ -59,14 +57,14 @@
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"buildTarget": "routeros-backup-manager-next-ui:build"
|
||||
"buildTarget": "mikrotik-backup-system-ui:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "routeros-backup-manager-next-ui:build:production"
|
||||
"buildTarget": "mikrotik-backup-system-ui:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "routeros-backup-manager-next-ui:build:development"
|
||||
"buildTarget": "mikrotik-backup-system-ui:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
|
||||
9710
frontend/package-lock.json
generated
9710
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "routeros-backup-manager-next-ui",
|
||||
"name": "mikrotik-backup-system-ui",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -11,27 +11,28 @@
|
||||
"build:dev": "ng build --configuration development"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "^17.3.0",
|
||||
"@angular/common": "^17.3.0",
|
||||
"@angular/compiler": "^17.3.0",
|
||||
"@angular/core": "^17.3.0",
|
||||
"@angular/forms": "^17.3.0",
|
||||
"@angular/platform-browser": "^17.3.0",
|
||||
"@angular/platform-browser-dynamic": "^17.3.0",
|
||||
"@angular/router": "^17.3.0",
|
||||
"@ngx-translate/core": "^15.0.0",
|
||||
"@ngx-translate/http-loader": "^8.0.0",
|
||||
"@angular/animations": "^20.3.0",
|
||||
"@angular/common": "^20.3.0",
|
||||
"@angular/compiler": "^20.3.0",
|
||||
"@angular/core": "^20.3.0",
|
||||
"@angular/forms": "^20.3.0",
|
||||
"@angular/platform-browser": "^20.3.0",
|
||||
"@angular/platform-browser-dynamic": "^20.3.0",
|
||||
"@angular/router": "^20.3.0",
|
||||
"@ngx-translate/core": "^17.0.0",
|
||||
"@ngx-translate/http-loader": "^17.0.0",
|
||||
"primeicons": "^7.0.0",
|
||||
"primeng": "^17.18.0",
|
||||
"primeng": "^20.1.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"tslib": "^2.6.2",
|
||||
"zone.js": "^0.14.4"
|
||||
"tslib": "^2.8.0",
|
||||
"zone.js": "~0.15.0",
|
||||
"@primeuix/themes": "^1.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^17.3.0",
|
||||
"@angular/cli": "^17.3.0",
|
||||
"@angular/compiler-cli": "^17.3.0",
|
||||
"typescript": "^5.4.0",
|
||||
"@angular-devkit/build-angular": "^20.3.0",
|
||||
"@angular/cli": "^20.3.0",
|
||||
"@angular/compiler-cli": "^20.3.0",
|
||||
"typescript": "~5.8.0",
|
||||
"ansi-colors": "^4.1.3",
|
||||
"esbuild": "^0.25.0",
|
||||
"semver": "^7.7.1",
|
||||
|
||||
@@ -53,7 +53,7 @@ export class AppComponent {
|
||||
|
||||
readonly menuItems = [
|
||||
{ label: 'nav.dashboard', link: '/', icon: 'pi pi-home', exact: true },
|
||||
{ label: 'nav.routers', link: '/routers', icon: 'pi pi-server', exact: false },
|
||||
{ label: 'nav.routers', link: '/devices', 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.logs', link: '/logs', icon: 'pi pi-history', exact: false },
|
||||
@@ -108,11 +108,11 @@ export class AppComponent {
|
||||
}
|
||||
|
||||
private updatePageLabel(url: string) {
|
||||
if (url.startsWith('/routers/')) {
|
||||
if (url.startsWith('/devices/') || url.startsWith('/routers/')) {
|
||||
this.pageLabel = 'routers.detailTitle';
|
||||
return;
|
||||
}
|
||||
if (url.startsWith('/routers')) {
|
||||
if (url.startsWith('/devices') || url.startsWith('/routers')) {
|
||||
this.pageLabel = 'routers.title';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -17,8 +17,10 @@ export const routes: Routes = [
|
||||
{ path: 'register', component: RegisterPageComponent },
|
||||
{ path: 'change-password', canActivate: [authGuard], component: ChangePasswordPageComponent },
|
||||
{ path: '', canActivate: [authGuard], component: DashboardPageComponent },
|
||||
{ path: 'routers', canActivate: [authGuard], component: RoutersPageComponent },
|
||||
{ path: 'routers/:id', canActivate: [authGuard], component: RouterDetailPageComponent },
|
||||
{ path: 'devices', canActivate: [authGuard], component: RoutersPageComponent },
|
||||
{ path: 'devices/:id', canActivate: [authGuard], component: RouterDetailPageComponent },
|
||||
{ path: 'routers', redirectTo: 'devices', pathMatch: 'full' },
|
||||
{ path: 'routers/:id', redirectTo: 'devices/:id', pathMatch: 'full' },
|
||||
{ path: 'files', canActivate: [authGuard], component: FilesPageComponent },
|
||||
{ path: 'diff-configs', canActivate: [authGuard], component: DiffConfigsPageComponent },
|
||||
{ path: 'settings', canActivate: [authGuard], component: SettingsPageComponent },
|
||||
|
||||
@@ -19,7 +19,23 @@ export class ThemeService {
|
||||
|
||||
set(mode: 'light' | 'dark') {
|
||||
this.modeState.set(mode);
|
||||
document.body.classList.toggle('dark-theme', mode === 'dark');
|
||||
|
||||
const isDark = mode === 'dark';
|
||||
const html = document.documentElement;
|
||||
const body = document.body;
|
||||
|
||||
html.classList.toggle('dark-theme', isDark);
|
||||
body.classList.toggle('dark-theme', isDark);
|
||||
|
||||
html.setAttribute('data-theme', mode);
|
||||
body.setAttribute('data-theme', mode);
|
||||
html.style.colorScheme = isDark ? 'dark' : 'light';
|
||||
body.style.colorScheme = isDark ? 'dark' : 'light';
|
||||
|
||||
localStorage.setItem(this.key, mode);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
});
|
||||
}
|
||||
}
|
||||
134
frontend/src/app/core/theme-preset.ts
Normal file
134
frontend/src/app/core/theme-preset.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { definePreset } from '@primeuix/themes';
|
||||
import Lara from '@primeuix/themes/lara';
|
||||
|
||||
const AppPreset = definePreset(Lara, {
|
||||
primitive: {
|
||||
borderRadius: {
|
||||
none: '0',
|
||||
xs: '8px',
|
||||
sm: '10px',
|
||||
md: '12px',
|
||||
lg: '16px',
|
||||
xl: '20px'
|
||||
}
|
||||
},
|
||||
semantic: {
|
||||
primary: {
|
||||
50: '#f6eee8',
|
||||
100: '#ecd7c8',
|
||||
200: '#dfb79e',
|
||||
300: '#cf9571',
|
||||
400: '#b9754d',
|
||||
500: '#8d593a',
|
||||
600: '#794a30',
|
||||
700: '#653d28',
|
||||
800: '#533220',
|
||||
900: '#43291a',
|
||||
950: '#2a1910'
|
||||
},
|
||||
colorScheme: {
|
||||
light: {
|
||||
surface: {
|
||||
0: '#ffffff',
|
||||
50: '#f8f8f5',
|
||||
100: '#f1f1ed',
|
||||
200: '#e6e6e0',
|
||||
300: '#dfdfd8',
|
||||
400: '#d0d0c8',
|
||||
500: '#b7b7ae',
|
||||
600: '#8f8f86',
|
||||
700: '#6e6e67',
|
||||
800: '#4f4f49',
|
||||
900: '#31312d',
|
||||
950: '#1e1e1b'
|
||||
},
|
||||
content: {
|
||||
background: '#f8f8f5',
|
||||
hoverBackground: '#f1f1ed',
|
||||
borderColor: 'rgba(17, 20, 23, 0.12)',
|
||||
color: '#111417',
|
||||
hoverColor: '#111417'
|
||||
},
|
||||
formField: {
|
||||
background: 'rgba(255, 255, 255, 0.5)',
|
||||
disabledBackground: '#f1f1ed',
|
||||
borderColor: 'rgba(17, 20, 23, 0.2)',
|
||||
hoverBorderColor: '#8d593a',
|
||||
focusBorderColor: '#8d593a',
|
||||
color: '#111417',
|
||||
placeholderColor: '#5e666e',
|
||||
floatLabelColor: '#5e666e'
|
||||
},
|
||||
overlay: {
|
||||
select: {
|
||||
background: '#f8f8f5',
|
||||
borderColor: 'rgba(17, 20, 23, 0.12)',
|
||||
color: '#111417'
|
||||
},
|
||||
popover: {
|
||||
background: '#f8f8f5',
|
||||
borderColor: 'rgba(17, 20, 23, 0.12)',
|
||||
color: '#111417'
|
||||
},
|
||||
modal: {
|
||||
background: '#f8f8f5',
|
||||
borderColor: 'rgba(17, 20, 23, 0.12)',
|
||||
color: '#111417'
|
||||
}
|
||||
}
|
||||
},
|
||||
dark: {
|
||||
surface: {
|
||||
0: '#17212b',
|
||||
50: '#1d2733',
|
||||
100: '#222d3a',
|
||||
200: '#2d3947',
|
||||
300: '#3a4858',
|
||||
400: '#516173',
|
||||
500: '#6c7c8d',
|
||||
600: '#93a5b6',
|
||||
700: '#b7c7d6',
|
||||
800: '#dae4ec',
|
||||
900: '#edf2f7',
|
||||
950: '#f7fbff'
|
||||
},
|
||||
content: {
|
||||
background: '#1d2733',
|
||||
hoverBackground: '#222d3a',
|
||||
borderColor: 'rgba(146, 170, 194, 0.16)',
|
||||
color: '#dae4ec',
|
||||
hoverColor: '#dae4ec'
|
||||
},
|
||||
formField: {
|
||||
background: 'rgba(255, 255, 255, 0.03)',
|
||||
disabledBackground: '#222d3a',
|
||||
borderColor: 'rgba(146, 170, 194, 0.25)',
|
||||
hoverBorderColor: '#4b90d9',
|
||||
focusBorderColor: '#4b90d9',
|
||||
color: '#dae4ec',
|
||||
placeholderColor: '#93a5b6',
|
||||
floatLabelColor: '#93a5b6'
|
||||
},
|
||||
overlay: {
|
||||
select: {
|
||||
background: '#1d2733',
|
||||
borderColor: 'rgba(146, 170, 194, 0.16)',
|
||||
color: '#dae4ec'
|
||||
},
|
||||
popover: {
|
||||
background: '#1d2733',
|
||||
borderColor: 'rgba(146, 170, 194, 0.16)',
|
||||
color: '#dae4ec'
|
||||
},
|
||||
modal: {
|
||||
background: '#1d2733',
|
||||
borderColor: 'rgba(146, 170, 194, 0.16)',
|
||||
color: '#dae4ec'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default AppPreset;
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="stats-grid" *ngIf="data">
|
||||
<app-stat-card [label]="'dashboard.managedRouters' | translate" [value]="data.routers_count" [hint]="'dashboard.managedRoutersHint' | translate" [tag]="'dashboard.inventoryTag' | translate" icon="pi pi-server" iconClass="icon-blue"></app-stat-card>
|
||||
<app-stat-card [label]="'dashboard.exportsCard' | translate" [value]="data.export_count" [hint]="'dashboard.exportsHint' | translate" [tag]="'dashboard.textTag' | translate" severity="success" icon="pi pi-file-export" iconClass="icon-emerald"></app-stat-card>
|
||||
<app-stat-card [label]="'dashboard.binaryCard' | translate" [value]="data.binary_count" [hint]="'dashboard.binaryHint' | translate" [tag]="'dashboard.binaryTag' | translate" severity="warning" icon="pi pi-database" iconClass="icon-amber"></app-stat-card>
|
||||
<app-stat-card [label]="'dashboard.binaryCard' | translate" [value]="data.binary_count" [hint]="'dashboard.binaryHint' | translate" [tag]="'dashboard.binaryTag' | translate" severity="warn" icon="pi pi-database" iconClass="icon-amber"></app-stat-card>
|
||||
<app-stat-card [label]="'dashboard.allFilesCard' | translate" [value]="data.total_backups" [hint]="'dashboard.allFilesHint' | translate" [tag]="'dashboard.archiveTag' | translate" severity="info" icon="pi pi-folder" iconClass="icon-violet"></app-stat-card>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="stats-grid compact-grid">
|
||||
<app-stat-card [label]="'diffConfigs.exportsCard' | translate" [value]="availableExportsCount" [hint]="'diffConfigs.exportsCardHint' | translate" [tag]="'files.exportType' | translate" severity="success" icon="pi pi-file-export" iconClass="icon-emerald"></app-stat-card>
|
||||
<app-stat-card [label]="'diffConfigs.scopeCard' | translate" [value]="selectedRouterLabel" [hint]="'diffConfigs.scopeCardHint' | translate" [tag]="'diffConfigs.scopeTag' | translate" severity="info" icon="pi pi-server" iconClass="icon-blue"></app-stat-card>
|
||||
<app-stat-card [label]="'diffConfigs.readyCard' | translate" [value]="compareReady ? ('common.ok' | translate) : ('common.idle' | translate)" [hint]="'diffConfigs.readyCardHint' | translate" [tag]="'diffConfigs.readyTag' | translate" severity="warning" icon="pi pi-code" iconClass="icon-amber"></app-stat-card>
|
||||
<app-stat-card [label]="'diffConfigs.readyCard' | translate" [value]="compareReady ? ('common.ok' | translate) : ('common.idle' | translate)" [hint]="'diffConfigs.readyCardHint' | translate" [tag]="'diffConfigs.readyTag' | translate" severity="warn" icon="pi pi-code" iconClass="icon-amber"></app-stat-card>
|
||||
<app-stat-card [label]="'diffConfigs.lastDiffCard' | translate" [value]="lastDiffLabel" [hint]="'diffConfigs.lastDiffCardHint' | translate" [tag]="'diffConfigs.lastDiffTag' | translate" severity="secondary" icon="pi pi-history" iconClass="icon-violet"></app-stat-card>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<div class="diff-workspace__toolbar">
|
||||
<span class="form-field diff-workspace__router">
|
||||
<label>{{ 'files.routerLabel' | translate }}</label>
|
||||
<p-dropdown [appendTo]="'body'" [options]="routerOptions" [(ngModel)]="routerId" optionLabel="label" optionValue="value" (onChange)="load()"></p-dropdown>
|
||||
<p-select [appendTo]="'body'" [options]="routerOptions" [(ngModel)]="routerId" optionLabel="label" optionValue="value" (onChange)="load()"></p-select>
|
||||
</span>
|
||||
<div class="diff-workspace__actions">
|
||||
<button pButton type="button" severity="secondary" icon="pi pi-refresh" [label]="'common.reset' | translate" (click)="routerId = null; compareLeftId = null; compareRightId = null; load()"></button>
|
||||
@@ -31,7 +31,7 @@
|
||||
<strong>{{ 'files.compareOlder' | translate }}</strong>
|
||||
<p-tag [value]="compareLeft ? ('common.ok' | translate) : ('diffConfigs.waitingTag' | translate)" [severity]="compareLeft ? 'success' : 'secondary'"></p-tag>
|
||||
</div>
|
||||
<p-dropdown [appendTo]="'body'" [options]="compareOptions" [(ngModel)]="compareLeftId" optionLabel="label" optionValue="value" [placeholder]="'files.pickOlder' | translate"></p-dropdown>
|
||||
<p-select [appendTo]="'body'" [options]="compareOptions" [(ngModel)]="compareLeftId" optionLabel="label" optionValue="value" [placeholder]="'files.pickOlder' | translate"></p-select>
|
||||
<div class="diff-pick-card__meta" *ngIf="compareLeft as item">
|
||||
<strong>{{ item.file_name }}</strong>
|
||||
<small>{{ item.router_name || item.router_id }} · {{ relativeAge(item.created_at) }}</small>
|
||||
@@ -48,7 +48,7 @@
|
||||
<strong>{{ 'files.compareNewer' | translate }}</strong>
|
||||
<p-tag [value]="compareRight ? ('common.ok' | translate) : ('diffConfigs.waitingTag' | translate)" [severity]="compareRight ? 'success' : 'secondary'"></p-tag>
|
||||
</div>
|
||||
<p-dropdown [appendTo]="'body'" [options]="compareOptions" [(ngModel)]="compareRightId" optionLabel="label" optionValue="value" [placeholder]="'files.pickNewer' | translate"></p-dropdown>
|
||||
<p-select [appendTo]="'body'" [options]="compareOptions" [(ngModel)]="compareRightId" optionLabel="label" optionValue="value" [placeholder]="'files.pickNewer' | translate"></p-select>
|
||||
<div class="diff-pick-card__meta" *ngIf="compareRight as item">
|
||||
<strong>{{ item.file_name }}</strong>
|
||||
<small>{{ item.router_name || item.router_id }} · {{ relativeAge(item.created_at) }}</small>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { FormsModule } from '@angular/forms';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ButtonModule } from 'primeng/button';
|
||||
import { DialogModule } from 'primeng/dialog';
|
||||
import { DropdownModule } from 'primeng/dropdown';
|
||||
import { SelectModule } from 'primeng/select';
|
||||
import { TableModule } from 'primeng/table';
|
||||
import { TagModule } from 'primeng/tag';
|
||||
|
||||
@@ -53,7 +53,7 @@ interface BackupDiffResponse {
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, TranslateModule, ButtonModule, DialogModule, DropdownModule, TableModule, TagModule, PageHeaderComponent, SectionCardComponent, StatCardComponent],
|
||||
imports: [CommonModule, FormsModule, TranslateModule, ButtonModule, DialogModule, SelectModule, TableModule, TagModule, PageHeaderComponent, SectionCardComponent, StatCardComponent],
|
||||
templateUrl: './diff-configs-page.component.html'
|
||||
})
|
||||
export class DiffConfigsPageComponent implements OnInit {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<app-stat-card [label]="'files.visibleFiles' | translate" [value]="files.length" [hint]="'files.visibleFilesHint' | translate" [tag]="'files.liveTag' | translate" icon="pi pi-folder-open" iconClass="icon-blue"></app-stat-card>
|
||||
<app-stat-card [label]="'files.selected' | translate" [value]="selectedIds.length" [hint]="'files.selectedHint' | translate" [tag]="'files.batchTag' | translate" severity="secondary" icon="pi pi-check-square" iconClass="icon-violet"></app-stat-card>
|
||||
<app-stat-card [label]="'files.exportsCard' | translate" [value]="exportCount" [hint]="'files.exportsHint' | translate" [tag]="'dashboard.textTag' | translate" severity="success" icon="pi pi-file-export" iconClass="icon-emerald"></app-stat-card>
|
||||
<app-stat-card [label]="'files.binaryCard' | translate" [value]="binaryCount" [hint]="'files.binaryHint' | translate" [tag]="'dashboard.binaryTag' | translate" severity="warning" icon="pi pi-database" iconClass="icon-amber"></app-stat-card>
|
||||
<app-stat-card [label]="'files.binaryCard' | translate" [value]="binaryCount" [hint]="'files.binaryHint' | translate" [tag]="'dashboard.binaryTag' | translate" severity="warn" icon="pi pi-database" iconClass="icon-amber"></app-stat-card>
|
||||
</div>
|
||||
|
||||
<app-section-card [title]="'files.filtersTitle' | translate" [subtitle]="'files.filtersSubtitle' | translate">
|
||||
@@ -24,12 +24,12 @@
|
||||
|
||||
<span class="form-field">
|
||||
<label>{{ 'files.typeLabel' | translate }}</label>
|
||||
<p-dropdown [options]="typeOptions" [(ngModel)]="backupType" optionLabel="label" optionValue="value"></p-dropdown>
|
||||
<p-select [options]="typeOptions" [(ngModel)]="backupType" optionLabel="label" optionValue="value"></p-select>
|
||||
</span>
|
||||
|
||||
<span class="form-field">
|
||||
<label>{{ 'files.routerLabel' | translate }}</label>
|
||||
<p-dropdown [options]="routerOptions" [(ngModel)]="routerId" optionLabel="label" optionValue="value"></p-dropdown>
|
||||
<p-select [options]="routerOptions" [(ngModel)]="routerId" optionLabel="label" optionValue="value"></p-select>
|
||||
</span>
|
||||
|
||||
<span class="form-field">
|
||||
@@ -39,12 +39,12 @@
|
||||
|
||||
<span class="form-field">
|
||||
<label>{{ 'files.sortLabel' | translate }}</label>
|
||||
<p-dropdown [options]="sortOptions" [(ngModel)]="sortBy" optionLabel="label" optionValue="value"></p-dropdown>
|
||||
<p-select [options]="sortOptions" [(ngModel)]="sortBy" optionLabel="label" optionValue="value"></p-select>
|
||||
</span>
|
||||
|
||||
<span class="form-field">
|
||||
<label>{{ 'files.orderLabel' | translate }}</label>
|
||||
<p-dropdown [options]="orderOptions" [(ngModel)]="order" optionLabel="label" optionValue="value"></p-dropdown>
|
||||
<p-select [options]="orderOptions" [(ngModel)]="order" optionLabel="label" optionValue="value"></p-select>
|
||||
</span>
|
||||
|
||||
<div class="filters-actions repository-toolbar__actions">
|
||||
@@ -68,14 +68,14 @@
|
||||
<div class="repository-compare__grid">
|
||||
<div class="compare-strip__slot repository-compare__slot">
|
||||
<label>{{ 'files.compareOlder' | translate }}</label>
|
||||
<p-dropdown [appendTo]="'body'" [options]="compareOptions" [(ngModel)]="compareLeftId" optionLabel="label" optionValue="value" [placeholder]="'files.pickOlder' | translate"></p-dropdown>
|
||||
<p-select [appendTo]="'body'" [options]="compareOptions" [(ngModel)]="compareLeftId" optionLabel="label" optionValue="value" [placeholder]="'files.pickOlder' | translate"></p-select>
|
||||
</div>
|
||||
|
||||
<button pButton type="button" severity="secondary" icon="pi pi-sort-alt" styleClass="compare-strip__swap" (click)="swapCompare()" [disabled]="!compareLeftId && !compareRightId"></button>
|
||||
|
||||
<div class="compare-strip__slot repository-compare__slot">
|
||||
<label>{{ 'files.compareNewer' | translate }}</label>
|
||||
<p-dropdown [appendTo]="'body'" [options]="compareOptions" [(ngModel)]="compareRightId" optionLabel="label" optionValue="value" [placeholder]="'files.pickNewer' | translate"></p-dropdown>
|
||||
<p-select [appendTo]="'body'" [options]="compareOptions" [(ngModel)]="compareRightId" optionLabel="label" optionValue="value" [placeholder]="'files.pickNewer' | translate"></p-select>
|
||||
</div>
|
||||
|
||||
<div class="compare-strip__actions repository-compare__actions">
|
||||
@@ -111,7 +111,7 @@
|
||||
<div class="table-primary">{{ item.router_name || item.router_id }}</div>
|
||||
<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><p-tag [value]="item.backup_type === 'export' ? ('files.exportType' | translate) : ('files.binaryType' | translate)" [severity]="item.backup_type === 'export' ? 'success' : 'warn'"></p-tag></td>
|
||||
<td>
|
||||
<div class="table-primary">{{ item.created_at | date: 'dd.MM.yyyy HH:mm' }}</div>
|
||||
<small class="table-secondary">{{ relativeAge(item.created_at) }}</small>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { FormsModule } from '@angular/forms';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ButtonModule } from 'primeng/button';
|
||||
import { DialogModule } from 'primeng/dialog';
|
||||
import { DropdownModule } from 'primeng/dropdown';
|
||||
import { SelectModule } from 'primeng/select';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { TableModule } from 'primeng/table';
|
||||
import { TagModule } from 'primeng/tag';
|
||||
@@ -57,7 +57,7 @@ interface BackupDiffResponse {
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, TranslateModule, ButtonModule, DialogModule, DropdownModule, InputTextModule, TableModule, TagModule, PageHeaderComponent, SectionCardComponent, StatCardComponent],
|
||||
imports: [CommonModule, FormsModule, TranslateModule, ButtonModule, DialogModule, SelectModule, InputTextModule, TableModule, TagModule, PageHeaderComponent, SectionCardComponent, StatCardComponent],
|
||||
templateUrl: './files-page.component.html'
|
||||
})
|
||||
export class FilesPageComponent implements OnInit {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<div class="stats-grid compact-grid">
|
||||
<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.binaryLabel' | translate" [value]="binaryBackups.length" [hint]="'routers.binaryLabelHint' | translate" [tag]="'files.binaryType' | translate" severity="warn" 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?.effective_username || '-'" [hint]="'routers.sshUserHint' | translate" [tag]="'routers.accessTag' | translate" severity="secondary" icon="pi pi-user" iconClass="icon-violet"></app-stat-card>
|
||||
</div>
|
||||
|
||||
@@ -252,7 +252,7 @@ export class RouterDetailPageComponent implements OnInit {
|
||||
}
|
||||
this.deletingRouter = true;
|
||||
this.api.http.delete(`${this.api.baseUrl}/routers/${this.routerId}`).subscribe({
|
||||
next: () => this.router.navigate(['/routers']),
|
||||
next: () => this.router.navigate(['/devices']),
|
||||
complete: () => {
|
||||
this.deletingRouter = false;
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
</span>
|
||||
<span class="form-field">
|
||||
<label>{{ 'routers.deviceType' | translate }}</label>
|
||||
<p-dropdown [options]="deviceTypeOptions" formControlName="device_type" optionLabel="label" optionValue="value"></p-dropdown>
|
||||
<p-select [options]="deviceTypeOptions" formControlName="device_type" optionLabel="label" optionValue="value"></p-select>
|
||||
</span>
|
||||
<span class="form-field">
|
||||
<label>{{ 'routers.host' | translate }}</label>
|
||||
@@ -138,7 +138,7 @@
|
||||
</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>
|
||||
<textarea pTextarea formControlName="ssh_key" rows="8" [placeholder]="'routers.optionalPrivateKey' | translate"></textarea>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ 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 { SelectModule } from 'primeng/select';
|
||||
import { TextareaModule } from 'primeng/textarea';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { TableModule } from 'primeng/table';
|
||||
import { TagModule } from 'primeng/tag';
|
||||
@@ -42,9 +42,9 @@ interface RouterItem {
|
||||
TranslateModule,
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
DropdownModule,
|
||||
SelectModule,
|
||||
InputTextModule,
|
||||
InputTextareaModule,
|
||||
TextareaModule,
|
||||
TableModule,
|
||||
TagModule,
|
||||
PageHeaderComponent,
|
||||
@@ -163,7 +163,7 @@ export class RoutersPageComponent implements OnInit {
|
||||
}
|
||||
|
||||
open(id: number) {
|
||||
this.router.navigate(['/routers', id]);
|
||||
this.router.navigate(['/devices', id]);
|
||||
}
|
||||
|
||||
deviceTypeLabel(item: RouterItem): string {
|
||||
@@ -174,7 +174,7 @@ export class RoutersPageComponent implements OnInit {
|
||||
return item.effective_username || item.ssh_user || '—';
|
||||
}
|
||||
|
||||
primaryAccessTag(item: RouterItem): { value: string; severity: 'success' | 'warning' | 'secondary' | 'info' } {
|
||||
primaryAccessTag(item: RouterItem): { value: string; severity: 'success' | 'warn' | 'secondary' | 'info' } {
|
||||
if (item.device_type === 'switchos') {
|
||||
if (item.uses_global_switchos_credentials) {
|
||||
return { value: this.ui.instant('routers.defaultCredentials'), severity: 'info' };
|
||||
@@ -187,15 +187,15 @@ export class RoutersPageComponent implements OnInit {
|
||||
|
||||
return {
|
||||
value: item.ssh_password ? this.ui.instant('routers.passwordMode') : this.ui.instant('routers.noPassword'),
|
||||
severity: item.ssh_password ? 'warning' : 'secondary'
|
||||
severity: item.ssh_password ? 'warn' : 'secondary'
|
||||
};
|
||||
}
|
||||
|
||||
secondaryAccessTag(item: RouterItem): { value: string; severity: 'success' | 'warning' | 'secondary' | 'info' } {
|
||||
secondaryAccessTag(item: RouterItem): { value: string; severity: 'success' | 'warn' | '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'
|
||||
severity: item.has_effective_password ? 'warn' : 'secondary'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<div class="scheduler-card__grid">
|
||||
<span class="form-field">
|
||||
<label>{{ 'settings.scheduleMode' | translate }}</label>
|
||||
<p-dropdown [appendTo]="'body'" [options]="scheduleModeOptions" [(ngModel)]="scheduleEditors.export.mode" [ngModelOptions]="{ standalone: true }" optionLabel="label" optionValue="value"></p-dropdown>
|
||||
<p-select [appendTo]="'body'" [options]="scheduleModeOptions" [(ngModel)]="scheduleEditors.export.mode" [ngModelOptions]="{ standalone: true }" optionLabel="label" optionValue="value"></p-select>
|
||||
</span>
|
||||
<span class="form-field" *ngIf="scheduleEditors.export.mode !== 'custom' && scheduleEditors.export.mode !== 'disabled'">
|
||||
<label>{{ 'settings.scheduleTime' | translate }}</label>
|
||||
@@ -60,7 +60,7 @@
|
||||
</span>
|
||||
<span class="form-field" *ngIf="scheduleEditors.export.mode === 'weekly'">
|
||||
<label>{{ 'settings.scheduleWeekday' | translate }}</label>
|
||||
<p-dropdown [appendTo]="'body'" [options]="weekdayOptions" [(ngModel)]="scheduleEditors.export.weekday" [ngModelOptions]="{ standalone: true }" optionLabel="label" optionValue="value"></p-dropdown>
|
||||
<p-select [appendTo]="'body'" [options]="weekdayOptions" [(ngModel)]="scheduleEditors.export.weekday" [ngModelOptions]="{ standalone: true }" optionLabel="label" optionValue="value"></p-select>
|
||||
</span>
|
||||
<span class="form-field form-field--full" *ngIf="scheduleEditors.export.mode === 'custom'">
|
||||
<label>{{ 'settings.exportCron' | translate }}</label>
|
||||
@@ -81,7 +81,7 @@
|
||||
<div class="scheduler-card__grid">
|
||||
<span class="form-field">
|
||||
<label>{{ 'settings.scheduleMode' | translate }}</label>
|
||||
<p-dropdown [appendTo]="'body'" [options]="scheduleModeOptions" [(ngModel)]="scheduleEditors.binary.mode" [ngModelOptions]="{ standalone: true }" optionLabel="label" optionValue="value"></p-dropdown>
|
||||
<p-select [appendTo]="'body'" [options]="scheduleModeOptions" [(ngModel)]="scheduleEditors.binary.mode" [ngModelOptions]="{ standalone: true }" optionLabel="label" optionValue="value"></p-select>
|
||||
</span>
|
||||
<span class="form-field" *ngIf="scheduleEditors.binary.mode !== 'custom' && scheduleEditors.binary.mode !== 'disabled'">
|
||||
<label>{{ 'settings.scheduleTime' | translate }}</label>
|
||||
@@ -93,7 +93,7 @@
|
||||
</span>
|
||||
<span class="form-field" *ngIf="scheduleEditors.binary.mode === 'weekly'">
|
||||
<label>{{ 'settings.scheduleWeekday' | translate }}</label>
|
||||
<p-dropdown [appendTo]="'body'" [options]="weekdayOptions" [(ngModel)]="scheduleEditors.binary.weekday" [ngModelOptions]="{ standalone: true }" optionLabel="label" optionValue="value"></p-dropdown>
|
||||
<p-select [appendTo]="'body'" [options]="weekdayOptions" [(ngModel)]="scheduleEditors.binary.weekday" [ngModelOptions]="{ standalone: true }" optionLabel="label" optionValue="value"></p-select>
|
||||
</span>
|
||||
<span class="form-field form-field--full" *ngIf="scheduleEditors.binary.mode === 'custom'">
|
||||
<label>{{ 'settings.binaryCron' | translate }}</label>
|
||||
@@ -122,7 +122,7 @@
|
||||
</span>
|
||||
<span class="form-field">
|
||||
<label>{{ 'settings.scheduleMode' | translate }}</label>
|
||||
<p-dropdown [appendTo]="'body'" [options]="scheduleModeOptions" [(ngModel)]="scheduleEditors.retention.mode" [ngModelOptions]="{ standalone: true }" optionLabel="label" optionValue="value"></p-dropdown>
|
||||
<p-select [appendTo]="'body'" [options]="scheduleModeOptions" [(ngModel)]="scheduleEditors.retention.mode" [ngModelOptions]="{ standalone: true }" optionLabel="label" optionValue="value"></p-select>
|
||||
</span>
|
||||
<span class="form-field" *ngIf="scheduleEditors.retention.mode !== 'custom' && scheduleEditors.retention.mode !== 'disabled'">
|
||||
<label>{{ 'settings.scheduleTime' | translate }}</label>
|
||||
@@ -134,7 +134,7 @@
|
||||
</span>
|
||||
<span class="form-field" *ngIf="scheduleEditors.retention.mode === 'weekly'">
|
||||
<label>{{ 'settings.scheduleWeekday' | translate }}</label>
|
||||
<p-dropdown [appendTo]="'body'" [options]="weekdayOptions" [(ngModel)]="scheduleEditors.retention.weekday" [ngModelOptions]="{ standalone: true }" optionLabel="label" optionValue="value"></p-dropdown>
|
||||
<p-select [appendTo]="'body'" [options]="weekdayOptions" [(ngModel)]="scheduleEditors.retention.weekday" [ngModelOptions]="{ standalone: true }" optionLabel="label" optionValue="value"></p-select>
|
||||
</span>
|
||||
<span class="form-field form-field--full" *ngIf="scheduleEditors.retention.mode === 'custom'">
|
||||
<label>{{ 'settings.retentionCron' | translate }}</label>
|
||||
@@ -180,11 +180,11 @@
|
||||
<div class="form-grid-2">
|
||||
<span class="form-field">
|
||||
<label>{{ 'topbar.languageSelector' | translate }}</label>
|
||||
<p-dropdown [appendTo]="'body'" [autoDisplayFirst]="false" formControlName="preferred_language" [options]="languageOptions" optionLabel="label" optionValue="value" (onChange)="previewLanguage($event.value)"></p-dropdown>
|
||||
<p-select [appendTo]="'body'" formControlName="preferred_language" [options]="languageOptions" optionLabel="label" optionValue="value" (onChange)="previewLanguage($event.value)"></p-select>
|
||||
</span>
|
||||
<span class="form-field">
|
||||
<label>{{ 'settings.fontFamily' | translate }}</label>
|
||||
<p-dropdown [appendTo]="'body'" formControlName="preferred_font" [options]="fontOptions" optionLabel="label" optionValue="value" (onChange)="previewFont()"></p-dropdown>
|
||||
<p-select [appendTo]="'body'" formControlName="preferred_font" [options]="fontOptions" optionLabel="label" optionValue="value" (onChange)="previewFont()"></p-select>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -283,7 +283,7 @@
|
||||
<div class="form-field form-field--full">
|
||||
<label>{{ 'settings.globalSshPrivateKey' | translate }}</label>
|
||||
<textarea
|
||||
pInputTextarea
|
||||
pTextarea
|
||||
formControlName="global_ssh_key"
|
||||
rows="14"
|
||||
[placeholder]="(hasStoredSshKey && !sshKeyVisible && !clearStoredSshKey) ? ('settings.globalSshPrivateKeyHiddenPlaceholder' | translate) : ('settings.globalSshPrivateKeyPlaceholder' | translate)"
|
||||
|
||||
@@ -3,9 +3,9 @@ import { Component, OnDestroy, OnInit, effect, inject } from '@angular/core';
|
||||
import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ButtonModule } from 'primeng/button';
|
||||
import { DropdownModule } from 'primeng/dropdown';
|
||||
import { SelectModule } from 'primeng/select';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { InputTextareaModule } from 'primeng/inputtextarea';
|
||||
import { TextareaModule } from 'primeng/textarea';
|
||||
import { TagModule } from 'primeng/tag';
|
||||
import { Subject, finalize, forkJoin, takeUntil } from 'rxjs';
|
||||
|
||||
@@ -71,7 +71,7 @@ interface SettingsResponse {
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, ReactiveFormsModule, TranslateModule, ButtonModule, DropdownModule, InputTextModule, InputTextareaModule, TagModule, PageHeaderComponent],
|
||||
imports: [CommonModule, FormsModule, ReactiveFormsModule, TranslateModule, ButtonModule, SelectModule, InputTextModule, TextareaModule, TagModule, PageHeaderComponent],
|
||||
templateUrl: './settings-page.component.html'
|
||||
})
|
||||
export class SettingsPageComponent implements OnInit, OnDestroy {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<app-page-header [eyebrow]="'switchosBeta.eyebrow' | translate" [title]="'switchosBeta.title' | translate" [subtitle]="'switchosBeta.subtitle' | translate">
|
||||
<div header-actions class="header-actions-row">
|
||||
<p-tag severity="warning" [value]="'switchosBeta.betaTag' | translate"></p-tag>
|
||||
<p-tag severity="warn" [value]="'switchosBeta.betaTag' | translate"></p-tag>
|
||||
</div>
|
||||
</app-page-header>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<strong>{{ 'switchosBeta.warningHeadline' | translate }}</strong>
|
||||
<p>{{ 'switchosBeta.warningBody' | translate }}</p>
|
||||
</div>
|
||||
<p-tag severity="warning" [value]="'switchosBeta.betaTag' | translate"></p-tag>
|
||||
<p-tag severity="warn" [value]="'switchosBeta.betaTag' | translate"></p-tag>
|
||||
</div>
|
||||
</app-section-card>
|
||||
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
<i [class]="icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
<p-tag *ngIf="tag" [value]="tag" [severity]="severity"></p-tag>
|
||||
<p-tag *ngIf="tag" [value]="tag" [severity]="tagSeverity"></p-tag>
|
||||
</p-card>
|
||||
|
||||
@@ -16,5 +16,9 @@ export class StatCardComponent {
|
||||
@Input() tag = '';
|
||||
@Input() icon = 'pi pi-chart-bar';
|
||||
@Input() iconClass = '';
|
||||
@Input() severity: 'success' | 'info' | 'warning' | 'danger' | 'secondary' | 'contrast' | undefined = 'info';
|
||||
@Input() severity: 'success' | 'info' | 'warning' | 'warn' | 'danger' | 'secondary' | 'contrast' | undefined = 'info';
|
||||
|
||||
get tagSeverity(): 'success' | 'secondary' | 'info' | 'warn' | 'danger' | 'contrast' | null | undefined {
|
||||
return this.severity === 'warning' ? 'warn' : this.severity;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
"menu": "Menu"
|
||||
},
|
||||
"sidebar": {
|
||||
"title": "MikroTik backup",
|
||||
"subtitle": "RouterOS manager"
|
||||
"title": "Mikrotik Backup System",
|
||||
"subtitle": "Device backup platform"
|
||||
},
|
||||
"topbar": {
|
||||
"caption": "mikrotik / control center",
|
||||
@@ -127,7 +127,7 @@
|
||||
"storageViewMixHint": "Split of all snapshots into text exports and binary backups.",
|
||||
"storageViewActivity": "7-day activity",
|
||||
"storageViewActivityHint": "Number of new backups created during the last seven days.",
|
||||
"storageViewRouters": "Top routers",
|
||||
"storageViewRouters": "Top devices",
|
||||
"storageViewRoutersHint": "Devices with the highest number of snapshots in the repository.",
|
||||
"storageChartEmpty": "There is not enough data to draw this chart yet.",
|
||||
"storageSnapshotTitle": "Repository metrics",
|
||||
@@ -299,8 +299,8 @@
|
||||
"compareSubtitle": "Pick two .rsc files and launch the diff without digging through the whole table.",
|
||||
"exportPoolLabel": "exports ready to compare",
|
||||
"compareSelectionHint": "Pick an older and a newer file",
|
||||
"compareReadySameRouter": "Pair ready · router {{router}}",
|
||||
"compareReadyMixedRouters": "Pair ready · mixed routers"
|
||||
"compareReadySameRouter": "Pair ready · device {{router}}",
|
||||
"compareReadyMixedRouters": "Pair ready · mixed devices"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
@@ -436,8 +436,8 @@
|
||||
"selectedBackupsDeleted": "Selected backups deleted.",
|
||||
"diffLoaded": "Diff loaded.",
|
||||
"archivePrepared": "Archive prepared.",
|
||||
"exportedRouters": "Export completed for {{count}} routers.",
|
||||
"binaryCompletedRouters": "Binary backup completed for {{count}} routers.",
|
||||
"exportedRouters": "Export completed for {{count}} devices.",
|
||||
"binaryCompletedRouters": "Binary backup completed for {{count}} devices.",
|
||||
"routerCreated": "Router created.",
|
||||
"routerUpdated": "Router updated.",
|
||||
"routerDeleted": "Router deleted.",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
},
|
||||
"sidebar": {
|
||||
"title": "copia de MikroTik",
|
||||
"subtitle": "gestor de RouterOS"
|
||||
"subtitle": "gestor de RouterOS/SwitchOS"
|
||||
},
|
||||
"topbar": {
|
||||
"caption": "mikrotik / centro de control",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
},
|
||||
"sidebar": {
|
||||
"title": "MikroTik-backup",
|
||||
"subtitle": "RouterOS-behandler"
|
||||
"subtitle": "RouterOS/SwitchOS-behandler"
|
||||
},
|
||||
"topbar": {
|
||||
"caption": "mikrotik / kontrollsenter",
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
"menu": "Menu"
|
||||
},
|
||||
"sidebar": {
|
||||
"title": "kopie MikroTik",
|
||||
"subtitle": "manager RouterOS"
|
||||
"title": "Mikrotik Backup System",
|
||||
"subtitle": "Device backup platform"
|
||||
},
|
||||
"topbar": {
|
||||
"caption": "mikrotik / control center",
|
||||
@@ -369,7 +369,7 @@
|
||||
"binaryPlannerHint": "Oddzielne okno dla pełnych backupów binarnych, gdy potrzebujesz punktów odtworzenia.",
|
||||
"retentionPlannerHint": "Retencja czyści stare backupy i logi według osobnego planu.",
|
||||
"connectionTestsTitle": "Automatyczne testy połączeń",
|
||||
"connectionTestsHint": "Aplikacja może sama odświeżać status routerów. Ustaw 0, aby wyłączyć automatyczne testy.",
|
||||
"connectionTestsHint": "Aplikacja może sama odświeżać status urządzeń. Ustaw 0, aby wyłączyć automatyczne testy.",
|
||||
"connectionTestIntervalMinutes": "Test co X minut",
|
||||
"connectionTestsEverySummary": "Co {{minutes}} min",
|
||||
"connectionTestsDisabledHint": "Automatyczne testy połączeń są wyłączone.",
|
||||
@@ -436,8 +436,8 @@
|
||||
"selectedBackupsDeleted": "Wybrane backupy zostały usunięte.",
|
||||
"diffLoaded": "Załadowano diff.",
|
||||
"archivePrepared": "Archiwum zostało przygotowane.",
|
||||
"exportedRouters": "Wykonano eksport dla {{count}} routerów.",
|
||||
"binaryCompletedRouters": "Wykonano backup binarny dla {{count}} routerów.",
|
||||
"exportedRouters": "Wykonano eksport dla {{count}} urządzeń.",
|
||||
"binaryCompletedRouters": "Wykonano backup binarny dla {{count}} urządzeń.",
|
||||
"routerCreated": "Router został dodany.",
|
||||
"routerUpdated": "Router został zaktualizowany.",
|
||||
"routerDeleted": "Router został usunięty.",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>RouterOS Backup Manager Next</title>
|
||||
<title>Mikrotik Backup System</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script>
|
||||
|
||||
@@ -1,32 +1,40 @@
|
||||
import { HttpClient, provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||
import { importProvidersFrom } from '@angular/core';
|
||||
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { provideAnimations } from '@angular/platform-browser/animations';
|
||||
import { ConfirmationService, MessageService } from 'primeng/api';
|
||||
import { providePrimeNG } from 'primeng/config';
|
||||
import AppPreset from './app/core/theme-preset';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
|
||||
import { provideTranslateService } from '@ngx-translate/core';
|
||||
import { provideTranslateHttpLoader } from '@ngx-translate/http-loader';
|
||||
|
||||
import { AppComponent } from './app/app.component';
|
||||
import { routes } from './app/app.routes';
|
||||
import { authInterceptor } from './app/core/interceptors/auth.interceptor';
|
||||
|
||||
export function httpLoaderFactory(http: HttpClient) {
|
||||
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
|
||||
}
|
||||
|
||||
bootstrapApplication(AppComponent, {
|
||||
providers: [
|
||||
provideAnimations(),
|
||||
provideHttpClient(withInterceptors([authInterceptor])),
|
||||
provideRouter(routes),
|
||||
providePrimeNG({
|
||||
theme: {
|
||||
preset: AppPreset,
|
||||
options: {
|
||||
darkModeSelector: '.dark-theme',
|
||||
cssLayer: false
|
||||
}
|
||||
}
|
||||
}),
|
||||
provideTranslateService({
|
||||
loader: provideTranslateHttpLoader({
|
||||
prefix: './assets/i18n/',
|
||||
suffix: '.json'
|
||||
}),
|
||||
lang: 'en',
|
||||
fallbackLang: 'en'
|
||||
}),
|
||||
MessageService,
|
||||
ConfirmationService,
|
||||
importProvidersFrom(
|
||||
TranslateModule.forRoot({
|
||||
defaultLanguage: 'pl',
|
||||
loader: { provide: TranslateLoader, useFactory: httpLoaderFactory, deps: [HttpClient] }
|
||||
})
|
||||
)
|
||||
ConfirmationService
|
||||
]
|
||||
}).catch((err) => console.error(err));
|
||||
|
||||
@@ -96,26 +96,12 @@ body.dark-theme .topbar {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.topbar__lang-select,
|
||||
.auth-toolbar__select {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
min-width: 10.5rem;
|
||||
min-height: 2.4rem;
|
||||
padding: 0.55rem 2.15rem 0.55rem 0.95rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: color-mix(in srgb, var(--surface-1) 92%, transparent);
|
||||
color: var(--text-main);
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.topbar__lang-picker::after,
|
||||
.auth-toolbar__select-wrap::after {
|
||||
content: '\e902';
|
||||
font-family: 'primeicons';
|
||||
position: absolute;
|
||||
right: 0.85rem;
|
||||
right: 1rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
@@ -123,15 +109,48 @@ body.dark-theme .topbar {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.topbar__lang-select,
|
||||
.auth-toolbar__select {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
width: 100%;
|
||||
min-width: 10.5rem;
|
||||
min-height: 2.95rem;
|
||||
padding: 0.72rem 2.7rem 0.72rem 1rem;
|
||||
border-radius: 14px;
|
||||
border: 1px solid var(--border-strong);
|
||||
background: color-mix(in srgb, var(--surface-1) 88%, white 12%);
|
||||
color: var(--text-main);
|
||||
font: inherit;
|
||||
font-size: 0.96rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.35;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.16s ease, box-shadow 0.16s ease, background-color 0.16s ease;
|
||||
}
|
||||
|
||||
.topbar__lang-select:hover,
|
||||
.auth-toolbar__select:hover {
|
||||
border-color: color-mix(in srgb, var(--accent) 56%, var(--border-strong));
|
||||
}
|
||||
|
||||
.topbar__lang-select:focus,
|
||||
.auth-toolbar__select:focus {
|
||||
outline: none;
|
||||
border-color: color-mix(in srgb, var(--blue) 72%, var(--border-strong));
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--blue) 18%, transparent);
|
||||
}
|
||||
|
||||
.topbar__lang-select option,
|
||||
.auth-toolbar__select option {
|
||||
color: #111417;
|
||||
background: #ffffff;
|
||||
color: var(--text-main);
|
||||
background: var(--surface-1);
|
||||
}
|
||||
|
||||
body.dark-theme .topbar__lang-select,
|
||||
body.dark-theme .auth-toolbar__select {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
body.dark-theme .topbar__lang-select option,
|
||||
@@ -140,24 +159,6 @@ body.dark-theme .auth-toolbar__select option {
|
||||
background: #1c2631;
|
||||
}
|
||||
|
||||
.app-auth-view {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-rows: minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.app-auth-view__content {
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.app-auth-view__content > * {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.layout-footer--auth {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.layout-shell,
|
||||
|
||||
@@ -262,7 +262,7 @@ body.dark-theme .topbar{
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.24);
|
||||
border-radius: 10px;
|
||||
border-radius: 12px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-family: var(--font-title);
|
||||
@@ -301,7 +301,7 @@ body.dark-theme .topbar{
|
||||
gap: 0.85rem;
|
||||
min-height: 44px;
|
||||
padding: 0.75rem 0.85rem;
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
color: rgba(230, 238, 245, 0.84);
|
||||
border: 1px solid transparent;
|
||||
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
|
||||
@@ -337,11 +337,6 @@ body.dark-theme .topbar{
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.layout-sidebar app-sidebar{
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.page-header{
|
||||
display: flex;
|
||||
@@ -759,7 +754,7 @@ body.dark-theme .app-table .p-datatable-tbody > tr:hover > td{
|
||||
}
|
||||
|
||||
.p-dropdown-panel .p-dropdown-items{
|
||||
padding: 0.35rem;
|
||||
padding: 0.45rem;
|
||||
}
|
||||
|
||||
.p-dropdown-panel .p-dropdown-item{
|
||||
@@ -1997,48 +1992,6 @@ body.dark-theme .settings-actions--sticky{
|
||||
}
|
||||
}
|
||||
|
||||
/* Normalize PrimeNG dropdown labels so selected values inherit the field style
|
||||
instead of rendering like nested inputs. */
|
||||
.p-dropdown{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 2.75rem;
|
||||
}
|
||||
|
||||
.p-dropdown .p-dropdown-label, .p-dropdown .p-dropdown-label.p-inputtext, .p-multiselect .p-multiselect-label{
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: inherit;
|
||||
font-weight: 500;
|
||||
line-height: 1.25;
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.p-dropdown .p-dropdown-label.p-placeholder, .p-multiselect .p-multiselect-label.p-placeholder{
|
||||
color: var(--text-soft);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.p-dropdown .p-dropdown-trigger, .p-multiselect .p-multiselect-trigger{
|
||||
display: grid;
|
||||
place-items: center;
|
||||
align-self: stretch;
|
||||
color: var(--text-soft);
|
||||
}
|
||||
|
||||
.p-dropdown-panel .p-dropdown-item, .p-multiselect-panel .p-multiselect-item{
|
||||
font-size: 0.84rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
/* patch set: settings, dashboard, repository, logs */
|
||||
.inline-summary{
|
||||
@@ -2270,12 +2223,6 @@ body.dark-theme .inline-summary{
|
||||
}
|
||||
|
||||
|
||||
.app-auth-view{
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.layout-footer a{
|
||||
@@ -2312,10 +2259,6 @@ body.dark-theme .inline-summary{
|
||||
box-shadow: 0 0 0 4px rgba(240, 180, 91, 0.14);
|
||||
}
|
||||
|
||||
.layout-footer--auth{
|
||||
margin-top: auto;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.api-connection-banner{
|
||||
position: fixed;
|
||||
@@ -2415,45 +2358,7 @@ body.dark-theme .api-connection-banner{
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
/* --- language selector + settings layout --- */
|
||||
.topbar__lang-picker{
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.topbar__lang-select{
|
||||
min-height: 2.5rem;
|
||||
padding: 0.55rem 2.15rem 0.55rem 0.85rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: color-mix(in srgb, var(--surface-1) 92%, transparent);
|
||||
color: var(--text-main);
|
||||
font: inherit;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
box-shadow: var(--shadow-sm);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
.topbar__lang-select{
|
||||
min-width: 132px;
|
||||
}
|
||||
|
||||
.topbar__lang-select option{
|
||||
background: var(--surface-1);
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
body.dark-theme .topbar__lang-select{
|
||||
background: rgba(15, 21, 29, 0.92);
|
||||
color: var(--text-main);
|
||||
border-color: var(--border-color);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
body.dark-theme .topbar__lang-select option{
|
||||
background: #1d2733;
|
||||
color: #dae4ec;
|
||||
}
|
||||
/* --- settings layout --- */
|
||||
|
||||
.settings-page-shell{
|
||||
display: grid;
|
||||
@@ -3584,3 +3489,329 @@ body.dark-theme .p-confirm-dialog .p-confirm-dialog-icon{
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* PrimeNG v20 compatibility bridge */
|
||||
.p-select,
|
||||
.p-dropdown,
|
||||
.p-multiselect,
|
||||
.p-inputtext,
|
||||
.p-textarea,
|
||||
.p-inputtextarea,
|
||||
textarea.p-inputtextarea,
|
||||
textarea.p-textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.p-button:not(.p-button-secondary):not(.p-button-help):not(.p-button-danger):not(.p-button-text):not(.p-button-outlined) {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: #f8fbff;
|
||||
}
|
||||
|
||||
body.dark-theme .p-button:not(.p-button-secondary):not(.p-button-help):not(.p-button-danger):not(.p-button-text):not(.p-button-outlined) {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: #17212b;
|
||||
}
|
||||
|
||||
.p-select,
|
||||
.p-dropdown,
|
||||
.p-textarea,
|
||||
.p-inputtextarea,
|
||||
.p-inputtext,
|
||||
textarea.p-inputtextarea,
|
||||
textarea.p-textarea {
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border-strong);
|
||||
background: color-mix(in srgb, var(--surface-1) 90%, white 10%);
|
||||
color: var(--text-main);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
body.dark-theme .p-select,
|
||||
body.dark-theme .p-dropdown,
|
||||
body.dark-theme .p-textarea,
|
||||
body.dark-theme .p-inputtextarea,
|
||||
body.dark-theme .p-inputtext,
|
||||
body.dark-theme textarea.p-inputtextarea,
|
||||
body.dark-theme textarea.p-textarea {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
color: var(--text-main);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.p-select,
|
||||
.p-dropdown {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 2.95rem;
|
||||
}
|
||||
|
||||
.p-select .p-select-label,
|
||||
.p-select .p-select-label.p-placeholder,
|
||||
.p-dropdown .p-dropdown-label,
|
||||
.p-dropdown .p-dropdown-label.p-inputtext,
|
||||
.p-dropdown .p-dropdown-label.p-placeholder,
|
||||
.p-multiselect .p-multiselect-label,
|
||||
.p-multiselect .p-multiselect-label.p-placeholder {
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
padding: 0.78rem 0 0.78rem 1rem;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.96rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.35;
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.p-select .p-select-label.p-placeholder,
|
||||
.p-dropdown .p-dropdown-label.p-placeholder,
|
||||
.p-multiselect .p-multiselect-label.p-placeholder {
|
||||
font-weight: 400;
|
||||
color: var(--text-soft);
|
||||
}
|
||||
|
||||
.repository-toolbar .p-select .p-select-label,
|
||||
.repository-compare .p-select .p-select-label,
|
||||
.diff-configs-compare .p-select .p-select-label,
|
||||
.repository-toolbar .p-dropdown .p-dropdown-label,
|
||||
.repository-compare .p-dropdown .p-dropdown-label {
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.p-select .p-select-dropdown,
|
||||
.p-dropdown .p-dropdown-trigger,
|
||||
.p-multiselect .p-multiselect-trigger {
|
||||
width: 2.85rem;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
align-self: stretch;
|
||||
color: var(--text-soft);
|
||||
flex: 0 0 2.85rem;
|
||||
}
|
||||
|
||||
.p-select.p-focus,
|
||||
.p-dropdown:not(.p-disabled).p-focus,
|
||||
.p-inputtext:enabled:focus,
|
||||
.p-textarea:enabled:focus,
|
||||
.p-inputtextarea:enabled:focus,
|
||||
textarea.p-inputtextarea:enabled:focus,
|
||||
textarea.p-textarea:enabled:focus {
|
||||
border-color: color-mix(in srgb, var(--blue) 72%, var(--border-strong));
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--blue) 18%, transparent);
|
||||
}
|
||||
|
||||
body.dark-theme .auth-card .p-inputtext,
|
||||
body.dark-theme .auth-card .p-password-input,
|
||||
body.dark-theme .auth-card .p-select,
|
||||
body.dark-theme .auth-card .p-textarea,
|
||||
body.dark-theme .auth-card .p-inputtextarea {
|
||||
color: var(--text-main);
|
||||
border-color: var(--border-color);
|
||||
background: rgba(255, 255, 255, 0.035);
|
||||
}
|
||||
|
||||
body.dark-theme .auth-card .p-inputtext::placeholder,
|
||||
body.dark-theme .auth-card .p-password-input::placeholder,
|
||||
body.dark-theme .auth-card .p-textarea::placeholder,
|
||||
body.dark-theme .auth-card .p-inputtextarea::placeholder {
|
||||
color: var(--text-soft);
|
||||
}
|
||||
|
||||
body.dark-theme .auth-card .p-inputtext:enabled:focus,
|
||||
body.dark-theme .auth-card .p-password-input:enabled:focus,
|
||||
body.dark-theme .auth-card .p-textarea:enabled:focus,
|
||||
body.dark-theme .auth-card .p-inputtextarea:enabled:focus {
|
||||
border-color: var(--blue);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--blue) 18%, transparent);
|
||||
}
|
||||
|
||||
.p-select-overlay,
|
||||
.p-dropdown-panel,
|
||||
.p-multiselect-panel,
|
||||
.p-component-overlay {
|
||||
backdrop-filter: blur(12px);
|
||||
background: color-mix(in srgb, var(--surface-1) 97%, white 3%);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-main);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
body.dark-theme .p-select-overlay,
|
||||
body.dark-theme .p-dropdown-panel,
|
||||
body.dark-theme .p-multiselect-panel,
|
||||
body.dark-theme .p-component-overlay {
|
||||
background: #1f2935;
|
||||
border-color: rgba(146, 170, 194, 0.2);
|
||||
}
|
||||
|
||||
.p-select-list,
|
||||
.p-dropdown-panel .p-dropdown-items,
|
||||
.p-multiselect-panel .p-multiselect-items {
|
||||
padding: 0.35rem;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
body.dark-theme .p-select-list,
|
||||
body.dark-theme .p-dropdown-panel .p-dropdown-items,
|
||||
body.dark-theme .p-multiselect-panel .p-multiselect-items {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.p-select-option,
|
||||
.p-dropdown-panel .p-dropdown-item,
|
||||
.p-multiselect-panel .p-multiselect-item {
|
||||
border-radius: 10px;
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.35;
|
||||
color: var(--text-main);
|
||||
transition: background-color 0.12s ease, color 0.12s ease;
|
||||
}
|
||||
|
||||
.p-select-option.p-focus,
|
||||
.p-select-option:hover,
|
||||
.p-dropdown-panel .p-dropdown-item.p-focus,
|
||||
.p-dropdown-panel .p-dropdown-item:hover,
|
||||
.p-multiselect-panel .p-multiselect-item.p-focus,
|
||||
.p-multiselect-panel .p-multiselect-item:hover {
|
||||
background: #dbe5f0;
|
||||
color: #13202c;
|
||||
}
|
||||
|
||||
.p-select-option.p-select-option-selected,
|
||||
.p-dropdown-panel .p-dropdown-item.p-highlight,
|
||||
.p-multiselect-panel .p-multiselect-item.p-highlight {
|
||||
background: #c8d8ea;
|
||||
color: #13202c;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
body.dark-theme .p-select-option,
|
||||
body.dark-theme .p-dropdown-panel .p-dropdown-item,
|
||||
body.dark-theme .p-multiselect-panel .p-multiselect-item {
|
||||
color: #dbe5ef;
|
||||
}
|
||||
|
||||
body.dark-theme .p-select-option.p-focus,
|
||||
body.dark-theme .p-select-option:hover,
|
||||
body.dark-theme .p-dropdown-panel .p-dropdown-item.p-focus,
|
||||
body.dark-theme .p-dropdown-panel .p-dropdown-item:hover,
|
||||
body.dark-theme .p-multiselect-panel .p-multiselect-item.p-focus,
|
||||
body.dark-theme .p-multiselect-panel .p-multiselect-item:hover {
|
||||
background: #314355;
|
||||
color: #f4f8fb;
|
||||
}
|
||||
|
||||
body.dark-theme .p-select-option.p-select-option-selected,
|
||||
body.dark-theme .p-dropdown-panel .p-dropdown-item.p-highlight,
|
||||
body.dark-theme .p-multiselect-panel .p-multiselect-item.p-highlight {
|
||||
background: #d9e1ea;
|
||||
color: #13202c;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.p-textarea,
|
||||
.p-inputtextarea,
|
||||
textarea.p-textarea,
|
||||
textarea.p-inputtextarea {
|
||||
padding: 0.82rem 0.9rem;
|
||||
}
|
||||
|
||||
.router-dialog.p-dialog {
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--surface-1);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
body.dark-theme .router-dialog.p-dialog {
|
||||
border-color: color-mix(in srgb, var(--border-color) 65%, transparent);
|
||||
}
|
||||
|
||||
.router-dialog .p-dialog-header {
|
||||
padding: 1.1rem 1.2rem 0.95rem;
|
||||
background: linear-gradient(180deg, color-mix(in srgb, var(--surface-1) 98%, transparent), color-mix(in srgb, var(--surface-0) 98%, transparent));
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.router-dialog .p-dialog-content {
|
||||
padding: 0 1.2rem 1.2rem;
|
||||
background: color-mix(in srgb, var(--surface-0) 98%, transparent);
|
||||
}
|
||||
|
||||
.router-dialog .p-dialog-header-icons {
|
||||
align-self: flex-start;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.router-dialog-panel {
|
||||
background: color-mix(in srgb, var(--surface-0) 96%, transparent);
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.router-dialog .p-select,
|
||||
.router-dialog .p-textarea,
|
||||
.router-dialog .p-inputtextarea,
|
||||
.router-dialog .p-inputtext {
|
||||
background: color-mix(in srgb, var(--surface-1) 90%, transparent);
|
||||
}
|
||||
|
||||
body.dark-theme .router-dialog .p-select,
|
||||
body.dark-theme .router-dialog .p-textarea,
|
||||
body.dark-theme .router-dialog .p-inputtextarea,
|
||||
body.dark-theme .router-dialog .p-inputtext,
|
||||
body.dark-theme .router-dialog-panel {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.repository-toolbar__search .p-input-icon-left {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.repository-toolbar__search .p-input-icon-left > i {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0.9rem;
|
||||
transform: translateY(-50%);
|
||||
margin: 0;
|
||||
z-index: 1;
|
||||
color: var(--text-soft);
|
||||
pointer-events: none;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.repository-toolbar__search .p-input-icon-left > .p-inputtext {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 2.75rem;
|
||||
padding-left: 2.5rem;
|
||||
}
|
||||
|
||||
|
||||
body.dark-theme .p-toast .p-toast-summary,
|
||||
body.dark-theme .p-toast .p-toast-detail,
|
||||
body.dark-theme .p-toast .p-toast-message-icon,
|
||||
body.dark-theme .p-toast .p-toast-icon-close {
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.repository-table .p-paginator .p-paginator-pages .p-paginator-page,
|
||||
.repository-table .p-paginator .p-paginator-next,
|
||||
.repository-table .p-paginator .p-paginator-prev,
|
||||
.repository-table .p-paginator .p-select {
|
||||
min-width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
1207
patch_routeros.py
1207
patch_routeros.py
File diff suppressed because it is too large
Load Diff
@@ -1,100 +0,0 @@
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
root = Path('/mnt/data/appcheck/backend')
|
||||
work = Path(tempfile.mkdtemp(prefix='rbmnext_'))
|
||||
os.environ['DATABASE_URL'] = f"sqlite:///{work/'test.db'}"
|
||||
os.environ['DATA_DIR'] = str(work/'data')
|
||||
os.environ['SECRET_KEY'] = 'test-secret'
|
||||
os.environ['DEFAULT_ADMIN_USERNAME'] = 'admin'
|
||||
os.environ['DEFAULT_ADMIN_PASSWORD'] = 'admin'
|
||||
os.environ['ALLOW_REGISTRATION'] = 'true'
|
||||
os.environ['CORS_ORIGINS'] = '["http://localhost:4200"]'
|
||||
os.chdir(root)
|
||||
import sys
|
||||
sys.path.insert(0, str(root))
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
from app.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
results = []
|
||||
|
||||
def record(name, ok, detail=''):
|
||||
results.append((name, ok, detail))
|
||||
|
||||
# health
|
||||
r = client.get('/api/health')
|
||||
record('health', r.status_code == 200 and r.json().get('status') == 'ok', str(r.status_code))
|
||||
|
||||
# login as default admin
|
||||
r = client.post('/api/auth/login', data={'username':'admin','password':'admin'})
|
||||
record('login_form', r.status_code == 200 and 'access_token' in r.json(), str(r.text))
|
||||
token = r.json()['access_token']
|
||||
headers = {'Authorization': f'Bearer {token}'}
|
||||
|
||||
# me
|
||||
r = client.get('/api/auth/me', headers=headers)
|
||||
record('auth_me', r.status_code == 200 and r.json()['username'] == 'admin', str(r.text))
|
||||
|
||||
# register new user and login json should fail (endpoint only form in current archive)
|
||||
r = client.post('/api/auth/register', json={'username':'u1','password':'p1234'})
|
||||
record('register', r.status_code == 200 and r.json()['username'] == 'u1', str(r.text))
|
||||
|
||||
# create router
|
||||
router_payload = {
|
||||
'name':'R1','host':'192.0.2.1','port':22,'ssh_user':'admin','ssh_password':'pass','ssh_key':''
|
||||
}
|
||||
r = client.post('/api/routers', json=router_payload, headers=headers)
|
||||
record('create_router', r.status_code == 200 and r.json()['name'] == 'R1', str(r.text))
|
||||
router_id = r.json()['id']
|
||||
|
||||
# list routers
|
||||
r = client.get('/api/routers', headers=headers)
|
||||
record('list_routers', r.status_code == 200 and len(r.json()) == 1, str(r.text))
|
||||
|
||||
# dashboard
|
||||
r = client.get('/api/dashboard', headers=headers)
|
||||
record('dashboard', r.status_code == 200 and r.json()['routers_count'] == 1, str(r.text))
|
||||
|
||||
# fake router tests and backups
|
||||
with patch('app.services.router_service.router_service.test_connection', return_value={'model':'hAP ax3','uptime':'1d','hostname':'r1'}):
|
||||
r = client.get(f'/api/routers/{router_id}/test-connection', headers=headers)
|
||||
record('test_connection_route', r.status_code == 200 and r.json()['hostname'] == 'r1', str(r.text))
|
||||
|
||||
with patch('app.services.router_service.router_service.export', return_value='/system identity set name=r1\n'):
|
||||
r = client.post(f'/api/backups/router/{router_id}/export', headers=headers, json={})
|
||||
record('export_router', r.status_code == 200 and r.json()['backup_type'] == 'export', str(r.text))
|
||||
export_id = r.json()['id']
|
||||
|
||||
with patch('app.services.router_service.router_service.binary_backup', side_effect=lambda router, base_name, path, key: Path(path).write_bytes(b'binary')):
|
||||
r = client.post(f'/api/backups/router/{router_id}/binary', headers=headers, json={})
|
||||
record('binary_backup', r.status_code == 200 and r.json()['backup_type'] == 'binary', str(r.text))
|
||||
binary_id = r.json()['id']
|
||||
|
||||
# view export
|
||||
r = client.get(f'/api/backups/{export_id}/view', headers=headers)
|
||||
record('view_export', r.status_code == 200 and 'content' in r.json(), str(r.text))
|
||||
|
||||
# download requires auth header at HTTP level; endpoint itself works with auth header
|
||||
r = client.get(f'/api/backups/{binary_id}/download', headers=headers)
|
||||
record('download_with_auth', r.status_code == 200 and r.content == b'binary', str(r.status_code))
|
||||
|
||||
# diff html endpoint works with auth header
|
||||
r = client.get(f'/api/backups/{export_id}/diff/{export_id}/html', headers=headers)
|
||||
record('diff_html_with_auth', r.status_code == 200 and '<html' in r.text.lower(), str(r.status_code))
|
||||
|
||||
# settings roundtrip
|
||||
r = client.get('/api/settings', headers=headers)
|
||||
record('get_settings', r.status_code == 200 and 'backup_retention_days' in r.json(), str(r.text))
|
||||
r = client.put('/api/settings', headers=headers, json={'backup_retention_days':10,'log_retention_days':5,'export_cron':'','binary_cron':'','retention_cron':'','enable_auto_export':False,'global_ssh_key':'','pushover_token':'','pushover_userkey':'','notify_failures_only':True,'smtp_host':'','smtp_port':587,'smtp_login':'','smtp_password':'','smtp_notifications_enabled':False,'recipient_email':None})
|
||||
record('put_settings', r.status_code == 200 and r.json()['backup_retention_days'] == 10, str(r.text))
|
||||
|
||||
for name, ok, detail in results:
|
||||
print(f"{name}: {'OK' if ok else 'FAIL'} {detail}")
|
||||
|
||||
if not all(ok for _,ok,_ in results):
|
||||
raise SystemExit(1)
|
||||
Reference in New Issue
Block a user