new features
This commit is contained in:
@@ -1,18 +1,23 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from pydantic import BaseModel
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.api.deps import get_current_user, get_db
|
from app.api.deps import get_current_user, get_db
|
||||||
from app.models.router import Router
|
from app.models.router import Router
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.schemas.router import RouterCreate, RouterResponse, RouterTestConnection, RouterUpdate
|
from app.schemas.router import RouterCreate, RouterPingStatus, RouterResponse, RouterTestConnection, RouterUpdate
|
||||||
from app.services.router_service import router_service
|
from app.services.router_service import router_service
|
||||||
from app.services.settings_service import settings_service
|
from app.services.settings_service import settings_service
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
class RouterPingBulkResponse(BaseModel):
|
||||||
|
items: list[RouterPingStatus]
|
||||||
|
|
||||||
|
|
||||||
def serialize_router(router: Router, global_settings) -> RouterResponse:
|
def serialize_router(router: Router, global_settings) -> RouterResponse:
|
||||||
has_router_key = bool((router.ssh_key or '').strip())
|
has_router_key = bool((router.ssh_key or '').strip())
|
||||||
has_global_key = bool((global_settings.global_ssh_key or '').strip())
|
has_global_key = bool((global_settings.global_ssh_key or '').strip())
|
||||||
@@ -49,6 +54,12 @@ def list_routers(current_user: User = Depends(get_current_user), db: Session = D
|
|||||||
return [serialize_router(router, global_settings) for router in routers]
|
return [serialize_router(router, global_settings) for router in routers]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/ping-statuses', response_model=RouterPingBulkResponse)
|
||||||
|
def list_ping_statuses(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||||
|
routers = db.query(Router).filter(Router.owner_id == current_user.id).all()
|
||||||
|
return RouterPingBulkResponse(items=router_service.ping_many(routers))
|
||||||
|
|
||||||
|
|
||||||
@router.post('', response_model=RouterResponse)
|
@router.post('', response_model=RouterResponse)
|
||||||
def create_router(payload: RouterCreate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
|
def create_router(payload: RouterCreate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||||
router_data = payload.model_dump()
|
router_data = payload.model_dump()
|
||||||
|
|||||||
@@ -61,6 +61,10 @@ def _run_lightweight_migrations() -> None:
|
|||||||
_ensure_column('routers', 'last_connection_auth_mode', 'VARCHAR(64)')
|
_ensure_column('routers', 'last_connection_auth_mode', 'VARCHAR(64)')
|
||||||
_ensure_column('routers', 'last_connection_http_status', 'VARCHAR(32)')
|
_ensure_column('routers', 'last_connection_http_status', 'VARCHAR(32)')
|
||||||
_ensure_column('routers', 'last_connection_backup_available', 'BOOLEAN')
|
_ensure_column('routers', 'last_connection_backup_available', 'BOOLEAN')
|
||||||
|
_ensure_column('routers', 'disable_all_backups', 'BOOLEAN DEFAULT 0 NOT NULL')
|
||||||
|
_ensure_column('routers', 'disable_export_backups', 'BOOLEAN DEFAULT 0 NOT NULL')
|
||||||
|
_ensure_column('routers', 'disable_binary_backups', 'BOOLEAN DEFAULT 0 NOT NULL')
|
||||||
|
_ensure_column('routers', 'disable_ping', 'BOOLEAN DEFAULT 0 NOT NULL')
|
||||||
|
|
||||||
|
|
||||||
def init_db():
|
def init_db():
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ class Router(Base):
|
|||||||
last_connection_auth_mode = Column(String(64), nullable=True)
|
last_connection_auth_mode = Column(String(64), nullable=True)
|
||||||
last_connection_http_status = Column(String(32), nullable=True)
|
last_connection_http_status = Column(String(32), nullable=True)
|
||||||
last_connection_backup_available = Column(Boolean, nullable=True)
|
last_connection_backup_available = Column(Boolean, nullable=True)
|
||||||
|
disable_all_backups = Column(Boolean, nullable=False, default=False)
|
||||||
|
disable_export_backups = Column(Boolean, nullable=False, default=False)
|
||||||
|
disable_binary_backups = Column(Boolean, nullable=False, default=False)
|
||||||
|
disable_ping = Column(Boolean, nullable=False, default=False)
|
||||||
created_at = Column(DateTime, server_default=func.now(), nullable=False)
|
created_at = Column(DateTime, server_default=func.now(), nullable=False)
|
||||||
|
|
||||||
backups = relationship("Backup", back_populates="router", cascade="all, delete-orphan")
|
backups = relationship("Backup", back_populates="router", cascade="all, delete-orphan")
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ class RouterBase(BaseModel):
|
|||||||
ssh_user: str | None = Field(default=None, max_length=120)
|
ssh_user: str | None = Field(default=None, max_length=120)
|
||||||
ssh_key: str | None = None
|
ssh_key: str | None = None
|
||||||
ssh_password: str | None = None
|
ssh_password: str | None = None
|
||||||
|
disable_all_backups: bool = False
|
||||||
|
disable_export_backups: bool = False
|
||||||
|
disable_binary_backups: bool = False
|
||||||
|
disable_ping: bool = False
|
||||||
|
|
||||||
@field_validator("name")
|
@field_validator("name")
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -54,6 +58,10 @@ class RouterUpdate(BaseModel):
|
|||||||
ssh_user: str | None = None
|
ssh_user: str | None = None
|
||||||
ssh_key: str | None = None
|
ssh_key: str | None = None
|
||||||
ssh_password: str | None = None
|
ssh_password: str | None = None
|
||||||
|
disable_all_backups: bool | None = None
|
||||||
|
disable_export_backups: bool | None = None
|
||||||
|
disable_binary_backups: bool | None = None
|
||||||
|
disable_ping: bool | None = None
|
||||||
|
|
||||||
@field_validator("name", "host", "ssh_user", "ssh_key", "ssh_password", mode="before")
|
@field_validator("name", "host", "ssh_user", "ssh_key", "ssh_password", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -72,6 +80,10 @@ class RouterResponse(RouterBase):
|
|||||||
has_effective_password: bool = False
|
has_effective_password: bool = False
|
||||||
supports_export: bool = False
|
supports_export: bool = False
|
||||||
supports_restore_upload: bool = False
|
supports_restore_upload: bool = False
|
||||||
|
disable_all_backups: bool = False
|
||||||
|
disable_export_backups: bool = False
|
||||||
|
disable_binary_backups: bool = False
|
||||||
|
disable_ping: bool = False
|
||||||
last_connection_status: bool | None = None
|
last_connection_status: bool | None = None
|
||||||
last_connection_tested_at: datetime | None = None
|
last_connection_tested_at: datetime | None = None
|
||||||
last_connection_error: str | None = None
|
last_connection_error: str | None = None
|
||||||
@@ -102,3 +114,10 @@ class RouterTestConnection(BaseModel):
|
|||||||
auth_mode: str | None = None
|
auth_mode: str | None = None
|
||||||
http_status: str | None = None
|
http_status: str | None = None
|
||||||
backup_available: bool | None = None
|
backup_available: bool | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class RouterPingStatus(BaseModel):
|
||||||
|
router_id: int
|
||||||
|
reachable: bool
|
||||||
|
latency_ms: float | None = None
|
||||||
|
disabled: bool = False
|
||||||
|
|||||||
@@ -191,6 +191,8 @@ class BackupService:
|
|||||||
router = self._router_for_user(db, user, router_id)
|
router = self._router_for_user(db, user, router_id)
|
||||||
if router.device_type != 'routeros':
|
if router.device_type != 'routeros':
|
||||||
raise HTTPException(status_code=400, detail='Text export is available only for RouterOS devices')
|
raise HTTPException(status_code=400, detail='Text export is available only for RouterOS devices')
|
||||||
|
if router.disable_all_backups or router.disable_export_backups:
|
||||||
|
raise HTTPException(status_code=400, detail='Exports are disabled for this device')
|
||||||
settings = settings_service.get_or_create(db)
|
settings = settings_service.get_or_create(db)
|
||||||
stamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
stamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||||
name = f'{router.name}_{router.id}_{stamp}.rsc'
|
name = f'{router.name}_{router.id}_{stamp}.rsc'
|
||||||
@@ -214,6 +216,8 @@ class BackupService:
|
|||||||
|
|
||||||
def binary_backup(self, db: Session, user: User, router_id: int) -> Backup:
|
def binary_backup(self, db: Session, user: User, router_id: int) -> Backup:
|
||||||
router = self._router_for_user(db, user, router_id)
|
router = self._router_for_user(db, user, router_id)
|
||||||
|
if router.disable_all_backups or router.disable_binary_backups:
|
||||||
|
raise HTTPException(status_code=400, detail='Binary backups are disabled for this device')
|
||||||
settings = settings_service.get_or_create(db)
|
settings = settings_service.get_or_create(db)
|
||||||
stamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
stamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||||
base_name = f'{router.name}_{router.id}_{stamp}'
|
base_name = f'{router.name}_{router.id}_{stamp}'
|
||||||
@@ -306,6 +310,13 @@ class BackupService:
|
|||||||
routers = db.query(Router).filter(Router.owner_id == user.id).all()
|
routers = db.query(Router).filter(Router.owner_id == user.id).all()
|
||||||
result = []
|
result = []
|
||||||
for router in routers:
|
for router in routers:
|
||||||
|
if router.disable_all_backups or router.disable_export_backups:
|
||||||
|
result.append({
|
||||||
|
'router': router.name,
|
||||||
|
'status': 'skipped',
|
||||||
|
'message': 'Exports are disabled for this device',
|
||||||
|
})
|
||||||
|
continue
|
||||||
if (router.device_type or 'routeros').lower() != 'routeros':
|
if (router.device_type or 'routeros').lower() != 'routeros':
|
||||||
result.append({
|
result.append({
|
||||||
'router': router.name,
|
'router': router.name,
|
||||||
@@ -324,6 +335,13 @@ class BackupService:
|
|||||||
routers = db.query(Router).filter(Router.owner_id == user.id).all()
|
routers = db.query(Router).filter(Router.owner_id == user.id).all()
|
||||||
result = []
|
result = []
|
||||||
for router in routers:
|
for router in routers:
|
||||||
|
if router.disable_all_backups or router.disable_binary_backups:
|
||||||
|
result.append({
|
||||||
|
'router': router.name,
|
||||||
|
'status': 'skipped',
|
||||||
|
'message': 'Binary backups are disabled for this device',
|
||||||
|
})
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
backup = self.binary_backup(db, user, router.id)
|
backup = self.binary_backup(db, user, router.id)
|
||||||
result.append({'router': router.name, 'status': 'ok', 'backup_id': backup.id})
|
result.append({'router': router.name, 'status': 'ok', 'backup_id': backup.id})
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import io
|
import io
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import platform
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
|
||||||
import paramiko
|
import paramiko
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
@@ -11,6 +15,30 @@ from app.services.swos_beta_service import swos_beta_service
|
|||||||
|
|
||||||
|
|
||||||
class RouterService:
|
class RouterService:
|
||||||
|
def ping(self, router: Router):
|
||||||
|
if getattr(router, 'disable_ping', False):
|
||||||
|
return {'router_id': router.id, 'reachable': False, 'latency_ms': None, 'disabled': True}
|
||||||
|
count_flag = '-n' if platform.system().lower().startswith('win') else '-c'
|
||||||
|
timeout_flag = '-w' if platform.system().lower().startswith('win') else '-W'
|
||||||
|
command = ['ping', count_flag, '1', timeout_flag, '1', router.host]
|
||||||
|
try:
|
||||||
|
completed = subprocess.run(command, capture_output=True, text=True, timeout=3, check=False)
|
||||||
|
output = completed.stdout + "\n" + completed.stderr
|
||||||
|
if completed.returncode != 0:
|
||||||
|
return {'router_id': router.id, 'reachable': False, 'latency_ms': None, 'disabled': False}
|
||||||
|
match = re.search(r'time[=<]\s*([0-9]+(?:[.,][0-9]+)?)\s*ms', output, re.IGNORECASE)
|
||||||
|
latency = float(match.group(1).replace(',', '.')) if match else None
|
||||||
|
return {'router_id': router.id, 'reachable': True, 'latency_ms': latency, 'disabled': False}
|
||||||
|
except Exception:
|
||||||
|
return {'router_id': router.id, 'reachable': False, 'latency_ms': None, 'disabled': False}
|
||||||
|
|
||||||
|
def ping_many(self, routers: list[Router]):
|
||||||
|
if not routers:
|
||||||
|
return []
|
||||||
|
max_workers = min(8, max(1, len(routers)))
|
||||||
|
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||||
|
return list(executor.map(self.ping, routers))
|
||||||
|
|
||||||
def _load_pkey(self, ssh_key_str: str):
|
def _load_pkey(self, ssh_key_str: str):
|
||||||
key_str = (ssh_key_str or "").strip()
|
key_str = (ssh_key_str or "").strip()
|
||||||
key_buffer = io.StringIO(key_str)
|
key_buffer = io.StringIO(key_str)
|
||||||
|
|||||||
@@ -212,6 +212,8 @@ class SchedulerService:
|
|||||||
with SessionLocal() as db:
|
with SessionLocal() as db:
|
||||||
routers = db.query(Router).all()
|
routers = db.query(Router).all()
|
||||||
for router in routers:
|
for router in routers:
|
||||||
|
if router.disable_all_backups or router.disable_export_backups:
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
backup_service.export_router(db, type('U', (), {'id': router.owner_id})(), router.id)
|
backup_service.export_router(db, type('U', (), {'id': router.owner_id})(), router.id)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -221,6 +223,8 @@ class SchedulerService:
|
|||||||
with SessionLocal() as db:
|
with SessionLocal() as db:
|
||||||
routers = db.query(Router).all()
|
routers = db.query(Router).all()
|
||||||
for router in routers:
|
for router in routers:
|
||||||
|
if router.disable_all_backups or router.disable_binary_backups:
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
backup_service.binary_backup(db, type('U', (), {'id': router.owner_id})(), router.id)
|
backup_service.binary_backup(db, type('U', (), {'id': router.owner_id})(), router.id)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|||||||
78
frontend/package-lock.json
generated
78
frontend/package-lock.json
generated
@@ -5193,9 +5193,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -5213,9 +5210,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -5233,9 +5227,6 @@
|
|||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -5253,9 +5244,6 @@
|
|||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -5273,9 +5261,6 @@
|
|||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -5293,9 +5278,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -5313,9 +5295,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -5851,9 +5830,6 @@
|
|||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -5875,9 +5851,6 @@
|
|||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -5899,9 +5872,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -5923,9 +5893,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -5947,9 +5914,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -5971,9 +5935,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -6189,9 +6150,6 @@
|
|||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -6206,9 +6164,6 @@
|
|||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -6223,9 +6178,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -6240,9 +6192,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -6257,9 +6206,6 @@
|
|||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -6274,9 +6220,6 @@
|
|||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -6291,9 +6234,6 @@
|
|||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -6308,9 +6248,6 @@
|
|||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -6325,9 +6262,6 @@
|
|||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -6342,9 +6276,6 @@
|
|||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -6359,9 +6290,6 @@
|
|||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -6376,9 +6304,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -6393,9 +6318,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|||||||
@@ -4,10 +4,11 @@
|
|||||||
[subtitle]="subtitle"
|
[subtitle]="subtitle"
|
||||||
>
|
>
|
||||||
<div header-actions class="header-actions-row">
|
<div header-actions class="header-actions-row">
|
||||||
<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-pencil" [label]="'common.edit' | translate" (click)="openEdit()"></button>
|
||||||
<button pButton type="button" severity="secondary" icon="pi pi-database" [label]="(isSwitchos ? 'routers.downloadSwitchBackup' : 'routers.binaryOne') | translate" [loading]="runningBinary" (click)="runBinary()"></button>
|
<button *ngIf="!isSwitchos" pButton type="button" icon="pi pi-upload" [label]="'routers.exportOne' | translate" [loading]="exporting" [disabled]="routerItem?.disable_all_backups || routerItem?.disable_export_backups" (click)="runExport()"></button>
|
||||||
|
<button pButton type="button" severity="secondary" icon="pi pi-database" [label]="(isSwitchos ? 'routers.downloadSwitchBackup' : 'routers.binaryOne') | translate" [loading]="runningBinary" [disabled]="routerItem?.disable_all_backups || routerItem?.disable_binary_backups" (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="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>
|
<button pButton type="button" severity="danger" icon="pi pi-trash" [label]="'routers.deleteDevice' | translate" [loading]="deletingRouter" (click)="deleteRouter()"></button>
|
||||||
</div>
|
</div>
|
||||||
</app-page-header>
|
</app-page-header>
|
||||||
|
|
||||||
@@ -46,7 +47,58 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</app-section-card>
|
</app-section-card>
|
||||||
|
|
||||||
<div class="router-detail-inspection-stack" *ngIf="!isSwitchos">
|
<app-section-card [title]="'routers.backupSettingsTitle' | translate" [subtitle]="'routers.backupSettingsHint' | translate">
|
||||||
|
<form [formGroup]="settingsForm" class="device-settings-form" (ngSubmit)="saveSettings()">
|
||||||
|
<div class="device-settings-stack">
|
||||||
|
<label class="device-toggle device-toggle--primary" [class.is-active]="settingsForm.controls.disable_all_backups.value">
|
||||||
|
<input type="checkbox" formControlName="disable_all_backups" />
|
||||||
|
<span class="device-toggle__switch" aria-hidden="true"></span>
|
||||||
|
<span class="device-toggle__icon"><i class="pi pi-ban"></i></span>
|
||||||
|
<span class="device-toggle__content">
|
||||||
|
<strong>{{ 'routers.disableAllBackups' | translate }}</strong>
|
||||||
|
<small>{{ 'routers.disableAllBackupsHint' | translate }}</small>
|
||||||
|
</span>
|
||||||
|
<span class="device-toggle__state">{{ (settingsForm.controls.disable_all_backups.value ? 'common.enabled' : 'common.disabled') | translate }}</span>
|
||||||
|
</label>
|
||||||
|
<label class="device-toggle" *ngIf="!isSwitchos" [class.is-active]="settingsForm.controls.disable_export_backups.value">
|
||||||
|
<input type="checkbox" formControlName="disable_export_backups" />
|
||||||
|
<span class="device-toggle__switch" aria-hidden="true"></span>
|
||||||
|
<span class="device-toggle__icon"><i class="pi pi-file-export"></i></span>
|
||||||
|
<span class="device-toggle__content">
|
||||||
|
<strong>{{ 'routers.disableExports' | translate }}</strong>
|
||||||
|
<small>{{ 'routers.disableExportsHint' | translate }}</small>
|
||||||
|
</span>
|
||||||
|
<span class="device-toggle__state">{{ (settingsForm.controls.disable_export_backups.value ? 'common.enabled' : 'common.disabled') | translate }}</span>
|
||||||
|
</label>
|
||||||
|
<label class="device-toggle" [class.is-active]="settingsForm.controls.disable_binary_backups.value">
|
||||||
|
<input type="checkbox" formControlName="disable_binary_backups" />
|
||||||
|
<span class="device-toggle__switch" aria-hidden="true"></span>
|
||||||
|
<span class="device-toggle__icon"><i class="pi pi-database"></i></span>
|
||||||
|
<span class="device-toggle__content">
|
||||||
|
<strong>{{ 'routers.disableBinaryBackups' | translate }}</strong>
|
||||||
|
<small>{{ 'routers.disableBinaryBackupsHint' | translate }}</small>
|
||||||
|
</span>
|
||||||
|
<span class="device-toggle__state">{{ (settingsForm.controls.disable_binary_backups.value ? 'common.enabled' : 'common.disabled') | translate }}</span>
|
||||||
|
</label>
|
||||||
|
<label class="device-toggle" [class.is-active]="settingsForm.controls.disable_ping.value">
|
||||||
|
<input type="checkbox" formControlName="disable_ping" />
|
||||||
|
<span class="device-toggle__switch" aria-hidden="true"></span>
|
||||||
|
<span class="device-toggle__icon"><i class="pi pi-wifi"></i></span>
|
||||||
|
<span class="device-toggle__content">
|
||||||
|
<strong>{{ 'routers.disablePing' | translate }}</strong>
|
||||||
|
<small>{{ 'routers.disablePingHint' | translate }}</small>
|
||||||
|
</span>
|
||||||
|
<span class="device-toggle__state">{{ (settingsForm.controls.disable_ping.value ? 'common.enabled' : 'common.disabled') | translate }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="dialog-actions device-settings-actions">
|
||||||
|
<button pButton type="submit" [loading]="savingSettings" [disabled]="savingSettings" [label]="'common.save' | translate"></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</app-section-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="router-detail-split-grid" *ngIf="!isSwitchos">
|
||||||
<app-section-card [title]="'routers.previewTitle' | translate" [subtitle]="'routers.previewSubtitle' | translate">
|
<app-section-card [title]="'routers.previewTitle' | translate" [subtitle]="'routers.previewSubtitle' | translate">
|
||||||
<div class="router-modal-summary" *ngIf="hasPreview; else noPreview">
|
<div class="router-modal-summary" *ngIf="hasPreview; else noPreview">
|
||||||
<div>
|
<div>
|
||||||
@@ -83,7 +135,6 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</app-section-card>
|
</app-section-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dashboard-grid router-detail-grid router-detail-grid--stack" *ngIf="!isSwitchos">
|
<div class="dashboard-grid router-detail-grid router-detail-grid--stack" *ngIf="!isSwitchos">
|
||||||
<app-section-card [title]="'routers.exportsTableTitle' | translate" [subtitle]="'routers.exportsTableSubtitle' | translate">
|
<app-section-card [title]="'routers.exportsTableTitle' | translate" [subtitle]="'routers.exportsTableSubtitle' | translate">
|
||||||
@@ -168,3 +219,84 @@
|
|||||||
<pre class="code-preview preview-dialog__content">{{ diffText }}</pre>
|
<pre class="code-preview preview-dialog__content">{{ diffText }}</pre>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</p-dialog>
|
</p-dialog>
|
||||||
|
|
||||||
|
|
||||||
|
<p-dialog [(visible)]="editVisible" [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">{{ 'routers.editDialogTitle' | translate }}</div>
|
||||||
|
<small>
|
||||||
|
{{
|
||||||
|
selectedDeviceType === 'switchos'
|
||||||
|
? ('routers.switchDialogSubtitle' | translate)
|
||||||
|
: ('routers.routerDialogSubtitle' | translate)
|
||||||
|
}}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<form [formGroup]="form" (ngSubmit)="saveEdit()" 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>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid-2 router-dialog-grid">
|
||||||
|
<span class="form-field">
|
||||||
|
<label>{{ 'routers.name' | translate }}</label>
|
||||||
|
<input pInputText formControlName="name" />
|
||||||
|
</span>
|
||||||
|
<span class="form-field">
|
||||||
|
<label>{{ 'routers.deviceType' | translate }}</label>
|
||||||
|
<p-select [appendTo]="'body'" [options]="deviceTypeOptions" formControlName="device_type" optionLabel="label" optionValue="value"></p-select>
|
||||||
|
</span>
|
||||||
|
<span class="form-field">
|
||||||
|
<label>{{ 'routers.host' | translate }}</label>
|
||||||
|
<input pInputText formControlName="host" />
|
||||||
|
</span>
|
||||||
|
<span class="form-field">
|
||||||
|
<label>{{ 'routers.port' | translate }}</label>
|
||||||
|
<input pInputText type="number" formControlName="port" />
|
||||||
|
</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>
|
||||||
|
</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 pTextarea formControlName="ssh_key" rows="8" [placeholder]="'routers.optionalPrivateKey' | translate"></textarea>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="dialog-actions router-dialog-actions">
|
||||||
|
<button pButton type="button" severity="secondary" [label]="'common.cancel' | translate" (click)="editVisible=false"></button>
|
||||||
|
<button pButton type="submit" [disabled]="form.invalid || saving" [loading]="saving" [label]="'routers.saveRouter' | translate"></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</p-dialog>
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { HttpResponse } from '@angular/common/http';
|
import { HttpResponse } from '@angular/common/http';
|
||||||
import { Component, OnInit, inject } from '@angular/core';
|
import { Component, OnInit, inject } from '@angular/core';
|
||||||
|
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { ButtonModule } from 'primeng/button';
|
import { ButtonModule } from 'primeng/button';
|
||||||
import { DialogModule } from 'primeng/dialog';
|
import { DialogModule } from 'primeng/dialog';
|
||||||
|
import { InputTextModule } from 'primeng/inputtext';
|
||||||
|
import { SelectModule } from 'primeng/select';
|
||||||
import { TableModule } from 'primeng/table';
|
import { TableModule } from 'primeng/table';
|
||||||
import { TagModule } from 'primeng/tag';
|
import { TagModule } from 'primeng/tag';
|
||||||
|
import { TextareaModule } from 'primeng/textarea';
|
||||||
|
|
||||||
import { ApiService } from '../../core/services/api.service';
|
import { ApiService } from '../../core/services/api.service';
|
||||||
import { UiService } from '../../core/services/ui.service';
|
import { UiService } from '../../core/services/ui.service';
|
||||||
@@ -22,9 +26,16 @@ interface DeviceItem {
|
|||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
device_type: DeviceType;
|
device_type: DeviceType;
|
||||||
|
ssh_user?: string | null;
|
||||||
|
ssh_password?: string | null;
|
||||||
|
ssh_key?: string | null;
|
||||||
effective_username?: string | null;
|
effective_username?: string | null;
|
||||||
supports_export: boolean;
|
supports_export: boolean;
|
||||||
supports_restore_upload: boolean;
|
supports_restore_upload: boolean;
|
||||||
|
disable_all_backups?: boolean;
|
||||||
|
disable_export_backups?: boolean;
|
||||||
|
disable_binary_backups?: boolean;
|
||||||
|
disable_ping?: boolean;
|
||||||
last_connection_status?: boolean | null;
|
last_connection_status?: boolean | null;
|
||||||
last_connection_tested_at?: string | null;
|
last_connection_tested_at?: string | null;
|
||||||
last_connection_error?: string | null;
|
last_connection_error?: string | null;
|
||||||
@@ -80,7 +91,21 @@ interface BackupDiffResponse {
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, TranslateModule, ButtonModule, DialogModule, TableModule, TagModule, PageHeaderComponent, SectionCardComponent, StatCardComponent],
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
TranslateModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
ButtonModule,
|
||||||
|
DialogModule,
|
||||||
|
TableModule,
|
||||||
|
TagModule,
|
||||||
|
InputTextModule,
|
||||||
|
SelectModule,
|
||||||
|
TextareaModule,
|
||||||
|
PageHeaderComponent,
|
||||||
|
SectionCardComponent,
|
||||||
|
StatCardComponent
|
||||||
|
],
|
||||||
templateUrl: './router-detail-page.component.html'
|
templateUrl: './router-detail-page.component.html'
|
||||||
})
|
})
|
||||||
export class RouterDetailPageComponent implements OnInit {
|
export class RouterDetailPageComponent implements OnInit {
|
||||||
@@ -88,6 +113,7 @@ export class RouterDetailPageComponent implements OnInit {
|
|||||||
private readonly api = inject(ApiService);
|
private readonly api = inject(ApiService);
|
||||||
private readonly router = inject(Router);
|
private readonly router = inject(Router);
|
||||||
private readonly ui = inject(UiService);
|
private readonly ui = inject(UiService);
|
||||||
|
private readonly fb = inject(FormBuilder);
|
||||||
|
|
||||||
routerId!: number;
|
routerId!: number;
|
||||||
routerItem: DeviceItem | null = null;
|
routerItem: DeviceItem | null = null;
|
||||||
@@ -98,16 +124,42 @@ export class RouterDetailPageComponent implements OnInit {
|
|||||||
previewTitle = '';
|
previewTitle = '';
|
||||||
previewVisible = false;
|
previewVisible = false;
|
||||||
diffVisible = false;
|
diffVisible = false;
|
||||||
|
editVisible = false;
|
||||||
diffData: BackupDiffResponse | null = null;
|
diffData: BackupDiffResponse | null = null;
|
||||||
exporting = false;
|
exporting = false;
|
||||||
runningBinary = false;
|
runningBinary = false;
|
||||||
testing = false;
|
testing = false;
|
||||||
deletingRouter = false;
|
deletingRouter = false;
|
||||||
|
saving = false;
|
||||||
|
savingSettings = false;
|
||||||
|
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'],
|
||||||
|
ssh_password: '',
|
||||||
|
ssh_key: ''
|
||||||
|
});
|
||||||
|
readonly settingsForm = this.fb.nonNullable.group({
|
||||||
|
disable_all_backups: false,
|
||||||
|
disable_export_backups: false,
|
||||||
|
disable_binary_backups: false,
|
||||||
|
disable_ping: false
|
||||||
|
});
|
||||||
|
|
||||||
get isSwitchos(): boolean {
|
get isSwitchos(): boolean {
|
||||||
return this.routerItem?.device_type === 'switchos';
|
return this.routerItem?.device_type === 'switchos';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get selectedDeviceType(): DeviceType {
|
||||||
|
return this.form.controls.device_type.value;
|
||||||
|
}
|
||||||
|
|
||||||
get exportBackups(): BackupItem[] {
|
get exportBackups(): BackupItem[] {
|
||||||
return this.backups.filter((item) => item.backup_type === 'export');
|
return this.backups.filter((item) => item.backup_type === 'export');
|
||||||
}
|
}
|
||||||
@@ -145,6 +197,14 @@ export class RouterDetailPageComponent implements OnInit {
|
|||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.routerId = Number(this.route.snapshot.paramMap.get('id'));
|
this.routerId = Number(this.route.snapshot.paramMap.get('id'));
|
||||||
|
this.form.controls.device_type.valueChanges.subscribe((deviceType) => {
|
||||||
|
this.applyDeviceDefaults((deviceType || 'routeros') as DeviceType);
|
||||||
|
});
|
||||||
|
this.settingsForm.controls.disable_all_backups.valueChanges.subscribe((disabled) => {
|
||||||
|
if (disabled) {
|
||||||
|
this.settingsForm.patchValue({ disable_export_backups: true, disable_binary_backups: true }, { emitEvent: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
this.load();
|
this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,12 +212,86 @@ export class RouterDetailPageComponent implements OnInit {
|
|||||||
this.api.http.get<DeviceItem>(`${this.api.baseUrl}/routers/${this.routerId}`).subscribe((routerItem) => {
|
this.api.http.get<DeviceItem>(`${this.api.baseUrl}/routers/${this.routerId}`).subscribe((routerItem) => {
|
||||||
this.routerItem = routerItem;
|
this.routerItem = routerItem;
|
||||||
this.connection = this.mapStoredConnection(routerItem);
|
this.connection = this.mapStoredConnection(routerItem);
|
||||||
|
this.patchSettingsForm(routerItem);
|
||||||
});
|
});
|
||||||
this.api.http.get<BackupItem[]>(`${this.api.baseUrl}/backups/router/${this.routerId}`).subscribe((r) => (this.backups = r));
|
this.api.http.get<BackupItem[]>(`${this.api.baseUrl}/backups/router/${this.routerId}`).subscribe((r) => (this.backups = r));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openEdit() {
|
||||||
|
if (!this.routerItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.form.reset({
|
||||||
|
name: this.routerItem.name,
|
||||||
|
device_type: this.routerItem.device_type,
|
||||||
|
host: this.routerItem.host,
|
||||||
|
port: this.routerItem.port,
|
||||||
|
ssh_user: this.routerItem.ssh_user ?? '',
|
||||||
|
ssh_password: this.routerItem.ssh_password ?? '',
|
||||||
|
ssh_key: this.routerItem.ssh_key ?? ''
|
||||||
|
});
|
||||||
|
this.editVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveEdit() {
|
||||||
|
if (this.form.invalid || this.saving) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.saving = true;
|
||||||
|
const payload = this.form.getRawValue();
|
||||||
|
if (payload.device_type === 'switchos') {
|
||||||
|
payload.ssh_key = '';
|
||||||
|
}
|
||||||
|
this.api.http.put<DeviceItem>(`${this.api.baseUrl}/routers/${this.routerId}`, payload).subscribe({
|
||||||
|
next: (routerItem) => {
|
||||||
|
this.routerItem = routerItem;
|
||||||
|
this.connection = this.mapStoredConnection(routerItem);
|
||||||
|
this.editVisible = false;
|
||||||
|
this.ui.success('toast.routerUpdated');
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
this.saving = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSettings() {
|
||||||
|
if (!this.routerItem || this.savingSettings) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.savingSettings = true;
|
||||||
|
const payload = this.settingsForm.getRawValue();
|
||||||
|
if (this.routerItem.device_type === 'switchos') {
|
||||||
|
payload.disable_export_backups = true;
|
||||||
|
}
|
||||||
|
if (payload.disable_all_backups) {
|
||||||
|
payload.disable_export_backups = true;
|
||||||
|
payload.disable_binary_backups = true;
|
||||||
|
}
|
||||||
|
this.api.http.put<DeviceItem>(`${this.api.baseUrl}/routers/${this.routerId}`, payload).subscribe({
|
||||||
|
next: (routerItem) => {
|
||||||
|
this.routerItem = routerItem;
|
||||||
|
this.connection = this.mapStoredConnection(routerItem);
|
||||||
|
this.patchSettingsForm(routerItem);
|
||||||
|
this.ui.success('toast.routerUpdated');
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
this.savingSettings = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private patchSettingsForm(item: DeviceItem) {
|
||||||
|
this.settingsForm.reset({
|
||||||
|
disable_all_backups: !!item.disable_all_backups,
|
||||||
|
disable_export_backups: !!item.disable_export_backups,
|
||||||
|
disable_binary_backups: !!item.disable_binary_backups,
|
||||||
|
disable_ping: !!item.disable_ping
|
||||||
|
}, { emitEvent: false });
|
||||||
|
}
|
||||||
|
|
||||||
runExport() {
|
runExport() {
|
||||||
if (this.exporting || this.isSwitchos) {
|
if (this.exporting || this.isSwitchos || this.routerItem?.disable_all_backups || this.routerItem?.disable_export_backups) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.exporting = true;
|
this.exporting = true;
|
||||||
@@ -173,7 +307,7 @@ export class RouterDetailPageComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
runBinary() {
|
runBinary() {
|
||||||
if (this.runningBinary) {
|
if (this.runningBinary || this.routerItem?.disable_all_backups || this.routerItem?.disable_binary_backups) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.runningBinary = true;
|
this.runningBinary = true;
|
||||||
@@ -332,6 +466,14 @@ export class RouterDetailPageComponent implements OnInit {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
|
||||||
private openBlob(response: HttpResponse<Blob>, fallbackName: string) {
|
private openBlob(response: HttpResponse<Blob>, fallbackName: string) {
|
||||||
const disposition = response.headers.get('content-disposition') || '';
|
const disposition = response.headers.get('content-disposition') || '';
|
||||||
const match = disposition.match(/filename="?([^";]+)"?/i);
|
const match = disposition.match(/filename="?([^";]+)"?/i);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<app-section-card [title]="'routers.listTitle' | translate" [subtitle]="'routers.listSubtitle' | translate">
|
<app-section-card [title]="'routers.listTitle' | translate" [subtitle]="'routers.listSubtitle' | translate">
|
||||||
<p-table [value]="routers" responsiveLayout="scroll" styleClass="app-table">
|
<p-table [value]="routers" responsiveLayout="scroll" styleClass="app-table">
|
||||||
<ng-template pTemplate="header">
|
<ng-template pTemplate="header">
|
||||||
<tr><th>{{ 'routers.name' | translate }}</th><th>{{ 'routers.endpoint' | translate }}</th><th>{{ 'routers.access' | translate }}</th><th>{{ 'common.actions' | translate }}</th></tr>
|
<tr><th>{{ 'routers.name' | translate }}</th><th>{{ 'routers.endpoint' | translate }}</th><th>{{ 'routers.access' | translate }}</th><th>{{ 'routers.backupPolicy' | translate }}</th><th>{{ 'routers.ping' | translate }}</th><th>{{ 'common.actions' | translate }}</th></tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template pTemplate="body" let-routerItem>
|
<ng-template pTemplate="body" let-routerItem>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -42,6 +42,13 @@
|
|||||||
<p-tag [value]="secondaryAccessTag(routerItem).value" [severity]="secondaryAccessTag(routerItem).severity"></p-tag>
|
<p-tag [value]="secondaryAccessTag(routerItem).value" [severity]="secondaryAccessTag(routerItem).severity"></p-tag>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="table-primary">{{ backupPolicyLabel(routerItem) }}</div>
|
||||||
|
<small class="table-secondary" *ngIf="routerItem.disable_all_backups">{{ 'common.disabled' | translate }}</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<small class="table-secondary">{{ pingLabel(routerItem) }}</small>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="table-actions table-actions--labels">
|
<div class="table-actions table-actions--labels">
|
||||||
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" icon="pi pi-arrow-right" [label]="'common.open' | translate" (click)="open(routerItem.id)"></button>
|
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" icon="pi pi-arrow-right" [label]="'common.open' | translate" (click)="open(routerItem.id)"></button>
|
||||||
|
|||||||
@@ -32,6 +32,17 @@ interface RouterItem {
|
|||||||
has_effective_ssh_key?: boolean;
|
has_effective_ssh_key?: boolean;
|
||||||
uses_global_switchos_credentials?: boolean;
|
uses_global_switchos_credentials?: boolean;
|
||||||
has_effective_password?: boolean;
|
has_effective_password?: boolean;
|
||||||
|
disable_all_backups?: boolean;
|
||||||
|
disable_export_backups?: boolean;
|
||||||
|
disable_binary_backups?: boolean;
|
||||||
|
disable_ping?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RouterPingStatus {
|
||||||
|
router_id: number;
|
||||||
|
reachable: boolean;
|
||||||
|
latency_ms?: number | null;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -62,6 +73,7 @@ export class RoutersPageComponent implements OnInit {
|
|||||||
editingId: number | null = null;
|
editingId: number | null = null;
|
||||||
saving = false;
|
saving = false;
|
||||||
routers: RouterItem[] = [];
|
routers: RouterItem[] = [];
|
||||||
|
pingStatuses: Record<number, RouterPingStatus> = {};
|
||||||
readonly deviceTypeOptions = [
|
readonly deviceTypeOptions = [
|
||||||
{ label: 'RouterOS', value: 'routeros' },
|
{ label: 'RouterOS', value: 'routeros' },
|
||||||
{ label: 'SwitchOS', value: 'switchos' }
|
{ label: 'SwitchOS', value: 'switchos' }
|
||||||
@@ -100,12 +112,31 @@ export class RoutersPageComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
load() {
|
load() {
|
||||||
this.api.http.get<RouterItem[]>(`${this.api.baseUrl}/routers`).subscribe((r) => (this.routers = r));
|
this.api.http.get<RouterItem[]>(`${this.api.baseUrl}/routers`).subscribe((r) => {
|
||||||
|
this.routers = r;
|
||||||
|
this.loadPingStatuses();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPingStatuses() {
|
||||||
|
this.api.http.get<{ items: RouterPingStatus[] }>(`${this.api.baseUrl}/routers/ping-statuses`).subscribe({
|
||||||
|
next: (response) => {
|
||||||
|
this.pingStatuses = response.items.reduce<Record<number, RouterPingStatus>>((acc, item) => {
|
||||||
|
acc[item.router_id] = item;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.pingStatuses = {};
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
openCreate() {
|
openCreate() {
|
||||||
this.editingId = null;
|
this.editingId = null;
|
||||||
this.form.reset({ name: '', device_type: 'routeros', 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;
|
this.visible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,6 +205,35 @@ export class RoutersPageComponent implements OnInit {
|
|||||||
return item.effective_username || item.ssh_user || '—';
|
return item.effective_username || item.ssh_user || '—';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pingLabel(item: RouterItem): string {
|
||||||
|
if (item.disable_ping) {
|
||||||
|
return this.ui.instant('routers.pingDisabled');
|
||||||
|
}
|
||||||
|
const ping = this.pingStatuses[item.id];
|
||||||
|
if (!ping) {
|
||||||
|
return this.ui.instant('routers.pingChecking');
|
||||||
|
}
|
||||||
|
if (!ping.reachable) {
|
||||||
|
return this.ui.instant('routers.noPing');
|
||||||
|
}
|
||||||
|
const value = typeof ping.latency_ms === 'number' ? Math.round(ping.latency_ms) : null;
|
||||||
|
return value === null ? this.ui.instant('routers.pingAvailable') : `${this.ui.instant('routers.ping')}: ${value} ms`;
|
||||||
|
}
|
||||||
|
|
||||||
|
backupPolicyLabel(item: RouterItem): string {
|
||||||
|
if (item.disable_all_backups) {
|
||||||
|
return this.ui.instant('routers.backupsDisabledAll');
|
||||||
|
}
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (!item.disable_export_backups && item.device_type === 'routeros') {
|
||||||
|
parts.push(this.ui.instant('routers.exportOne'));
|
||||||
|
}
|
||||||
|
if (!item.disable_binary_backups) {
|
||||||
|
parts.push(this.ui.instant('routers.binaryOne'));
|
||||||
|
}
|
||||||
|
return parts.length ? parts.join(' / ') : this.ui.instant('routers.backupsDisabledAll');
|
||||||
|
}
|
||||||
|
|
||||||
primaryAccessTag(item: RouterItem): { value: string; severity: 'success' | 'warn' | 'secondary' | 'info' } {
|
primaryAccessTag(item: RouterItem): { value: string; severity: 'success' | 'warn' | 'secondary' | 'info' } {
|
||||||
if (item.device_type === 'switchos') {
|
if (item.device_type === 'switchos') {
|
||||||
if (item.uses_global_switchos_credentials) {
|
if (item.uses_global_switchos_credentials) {
|
||||||
|
|||||||
@@ -31,7 +31,8 @@
|
|||||||
"desc": "Descending",
|
"desc": "Descending",
|
||||||
"enabled": "Enabled",
|
"enabled": "Enabled",
|
||||||
"disabled": "Disabled",
|
"disabled": "Disabled",
|
||||||
"failed": "Failed"
|
"failed": "Failed",
|
||||||
|
"save": "Save"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
@@ -175,7 +176,7 @@
|
|||||||
"exportOne": "Export",
|
"exportOne": "Export",
|
||||||
"binaryOne": "Binary",
|
"binaryOne": "Binary",
|
||||||
"testConnection": "Test connection",
|
"testConnection": "Test connection",
|
||||||
"deleteRouter": "Delete router",
|
"deleteRouter": "Delete device",
|
||||||
"exportsLabel": "Exports",
|
"exportsLabel": "Exports",
|
||||||
"exportsLabelHint": "Text snapshots",
|
"exportsLabelHint": "Text snapshots",
|
||||||
"binaryLabel": "Binary backups",
|
"binaryLabel": "Binary backups",
|
||||||
@@ -232,7 +233,25 @@
|
|||||||
"connectionSectionHint": "Basic device identity and endpoint used to reach it.",
|
"connectionSectionHint": "Basic device identity and endpoint used to reach it.",
|
||||||
"credentialsSectionTitle": "Access and credentials",
|
"credentialsSectionTitle": "Access and credentials",
|
||||||
"routerDialogSubtitle": "Set the device endpoint, SSH access data and your preferred login method.",
|
"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."
|
"switchDialogSubtitle": "Set the SwitchOS endpoint and optional local or shared credentials from settings.",
|
||||||
|
"backupPolicy": "Backups",
|
||||||
|
"backupSettingsTitle": "Backup settings",
|
||||||
|
"backupSettingsHint": "You can disable all backups or only selected backup types for this device.",
|
||||||
|
"deleteDevice": "Delete device",
|
||||||
|
"disableAllBackupsHint": "Automatically checks exports and binary backups.",
|
||||||
|
"disableExportsHint": "Blocks only text exports for this device.",
|
||||||
|
"disableBinaryBackupsHint": "Blocks only binary backups for this device.",
|
||||||
|
"disablePing": "Disable ping for this device",
|
||||||
|
"disablePingHint": "This device will not be pinged on the devices list.",
|
||||||
|
"pingDisabled": "Ping disabled",
|
||||||
|
"disableAllBackups": "Disable all backups for this device",
|
||||||
|
"disableExports": "Disable exports",
|
||||||
|
"disableBinaryBackups": "Disable binary backups",
|
||||||
|
"backupsDisabledAll": "All backups disabled",
|
||||||
|
"ping": "Ping",
|
||||||
|
"pingChecking": "Checking...",
|
||||||
|
"noPing": "No ping",
|
||||||
|
"pingAvailable": "Ping available"
|
||||||
},
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"title": "Repository",
|
"title": "Repository",
|
||||||
@@ -463,7 +482,7 @@
|
|||||||
"header": "Confirmation",
|
"header": "Confirmation",
|
||||||
"deleteBackup": "Delete this backup file?",
|
"deleteBackup": "Delete this backup file?",
|
||||||
"deleteSelectedFiles": "Delete {{count}} selected files?",
|
"deleteSelectedFiles": "Delete {{count}} selected files?",
|
||||||
"deleteRouterWithFiles": "Delete the router and all related files?",
|
"deleteRouterWithFiles": "Delete the device and all related files?",
|
||||||
"deleteLogsOlderThan": "Delete logs older than {{days}} days?"
|
"deleteLogsOlderThan": "Delete logs older than {{days}} days?"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
|
|||||||
@@ -31,7 +31,8 @@
|
|||||||
"desc": "Descendente",
|
"desc": "Descendente",
|
||||||
"enabled": "Activado",
|
"enabled": "Activado",
|
||||||
"disabled": "Desactivado",
|
"disabled": "Desactivado",
|
||||||
"failed": "Error"
|
"failed": "Error",
|
||||||
|
"save": "Guardar"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "Panel",
|
"dashboard": "Panel",
|
||||||
@@ -175,7 +176,7 @@
|
|||||||
"exportOne": "Exportar",
|
"exportOne": "Exportar",
|
||||||
"binaryOne": "Binario",
|
"binaryOne": "Binario",
|
||||||
"testConnection": "Probar conexión",
|
"testConnection": "Probar conexión",
|
||||||
"deleteRouter": "Eliminar router",
|
"deleteRouter": "Eliminar dispositivo",
|
||||||
"exportsLabel": "Exportaciones",
|
"exportsLabel": "Exportaciones",
|
||||||
"exportsLabelHint": "Instantáneas de texto",
|
"exportsLabelHint": "Instantáneas de texto",
|
||||||
"binaryLabel": "Copias binarias",
|
"binaryLabel": "Copias binarias",
|
||||||
@@ -463,7 +464,7 @@
|
|||||||
"header": "Confirmación",
|
"header": "Confirmación",
|
||||||
"deleteBackup": "¿Eliminar este archivo de copia?",
|
"deleteBackup": "¿Eliminar este archivo de copia?",
|
||||||
"deleteSelectedFiles": "¿Eliminar {{count}} archivos seleccionados?",
|
"deleteSelectedFiles": "¿Eliminar {{count}} archivos seleccionados?",
|
||||||
"deleteRouterWithFiles": "¿Eliminar el router y todos los archivos relacionados?",
|
"deleteRouterWithFiles": "¿Eliminar el dispositivo y todos los archivos relacionados?",
|
||||||
"deleteLogsOlderThan": "¿Eliminar registros anteriores a {{days}} días?"
|
"deleteLogsOlderThan": "¿Eliminar registros anteriores a {{days}} días?"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
|
|||||||
@@ -31,7 +31,8 @@
|
|||||||
"desc": "Synkende",
|
"desc": "Synkende",
|
||||||
"enabled": "På",
|
"enabled": "På",
|
||||||
"disabled": "Av",
|
"disabled": "Av",
|
||||||
"failed": "Feilet"
|
"failed": "Feilet",
|
||||||
|
"save": "Lagre"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "Dashbord",
|
"dashboard": "Dashbord",
|
||||||
@@ -175,7 +176,7 @@
|
|||||||
"exportOne": "Eksport",
|
"exportOne": "Eksport",
|
||||||
"binaryOne": "Binær",
|
"binaryOne": "Binær",
|
||||||
"testConnection": "Test tilkobling",
|
"testConnection": "Test tilkobling",
|
||||||
"deleteRouter": "Slett ruter",
|
"deleteRouter": "Slett enhet",
|
||||||
"exportsLabel": "Eksporter",
|
"exportsLabel": "Eksporter",
|
||||||
"exportsLabelHint": "Tekstbaserte øyeblikksbilder",
|
"exportsLabelHint": "Tekstbaserte øyeblikksbilder",
|
||||||
"binaryLabel": "Binære backuper",
|
"binaryLabel": "Binære backuper",
|
||||||
@@ -463,7 +464,7 @@
|
|||||||
"header": "Bekreftelse",
|
"header": "Bekreftelse",
|
||||||
"deleteBackup": "Slette denne backupfilen?",
|
"deleteBackup": "Slette denne backupfilen?",
|
||||||
"deleteSelectedFiles": "Slette {{count}} valgte filer?",
|
"deleteSelectedFiles": "Slette {{count}} valgte filer?",
|
||||||
"deleteRouterWithFiles": "Slette ruteren og alle relaterte filer?",
|
"deleteRouterWithFiles": "Slette enheten og alle relaterte filer?",
|
||||||
"deleteLogsOlderThan": "Slette logger eldre enn {{days}} dager?"
|
"deleteLogsOlderThan": "Slette logger eldre enn {{days}} dager?"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
|
|||||||
@@ -31,7 +31,8 @@
|
|||||||
"desc": "Malejąco",
|
"desc": "Malejąco",
|
||||||
"enabled": "Włączone",
|
"enabled": "Włączone",
|
||||||
"disabled": "Wyłączone",
|
"disabled": "Wyłączone",
|
||||||
"failed": "Błąd"
|
"failed": "Błąd",
|
||||||
|
"save": "Zapisz"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
@@ -102,7 +103,7 @@
|
|||||||
"activityTitle": "Ostatnia aktywność",
|
"activityTitle": "Ostatnia aktywność",
|
||||||
"activitySubtitle": "Najnowsze zdarzenia operacyjne z backendu.",
|
"activitySubtitle": "Najnowsze zdarzenia operacyjne z backendu.",
|
||||||
"noActivity": "Brak ostatnich zdarzeń do wyświetlenia.",
|
"noActivity": "Brak ostatnich zdarzeń do wyświetlenia.",
|
||||||
"avgBackupsPerRouter": "Śr. backupów / router",
|
"avgBackupsPerRouter": "Śr. backupów / urządzenie",
|
||||||
"activitySuccess": "Zadanie zakończone",
|
"activitySuccess": "Zadanie zakończone",
|
||||||
"activityFailure": "Wymaga uwagi",
|
"activityFailure": "Wymaga uwagi",
|
||||||
"activityMaintenance": "Utrzymanie",
|
"activityMaintenance": "Utrzymanie",
|
||||||
@@ -111,10 +112,10 @@
|
|||||||
"operationsSubtitle": "Główne akcje i szybkie wskaźniki pracy repozytorium.",
|
"operationsSubtitle": "Główne akcje i szybkie wskaźniki pracy repozytorium.",
|
||||||
"latestSnapshot": "Najnowszy snapshot",
|
"latestSnapshot": "Najnowszy snapshot",
|
||||||
"coverageLabel": "Pokrycie floty",
|
"coverageLabel": "Pokrycie floty",
|
||||||
"coverageHint": "Routery z co najmniej jednym backupem",
|
"coverageHint": "Urządzenia z co najmniej jednym backupem",
|
||||||
"weeklyActivityLabel": "Aktywność 7 dni",
|
"weeklyActivityLabel": "Aktywność 7 dni",
|
||||||
"weeklyActivityHint": "Nowe backupy z ostatniego tygodnia",
|
"weeklyActivityHint": "Nowe backupy z ostatniego tygodnia",
|
||||||
"busiestRouterLabel": "Najaktywniejszy router",
|
"busiestRouterLabel": "Najaktywniejsze urządzenie",
|
||||||
"routerSnapshotsHint": "{{count}} snapshotów w repozytorium",
|
"routerSnapshotsHint": "{{count}} snapshotów w repozytorium",
|
||||||
"exportShareLabel": "Udział eksportów",
|
"exportShareLabel": "Udział eksportów",
|
||||||
"activityTodayLabel": "Zdarzenia dzisiaj",
|
"activityTodayLabel": "Zdarzenia dzisiaj",
|
||||||
@@ -127,7 +128,7 @@
|
|||||||
"storageViewMixHint": "Podział wszystkich kopii na eksporty tekstowe i backupy binarne.",
|
"storageViewMixHint": "Podział wszystkich kopii na eksporty tekstowe i backupy binarne.",
|
||||||
"storageViewActivity": "Aktywność 7 dni",
|
"storageViewActivity": "Aktywność 7 dni",
|
||||||
"storageViewActivityHint": "Liczba nowych backupów z ostatnich siedmiu dni.",
|
"storageViewActivityHint": "Liczba nowych backupów z ostatnich siedmiu dni.",
|
||||||
"storageViewRouters": "Top routery",
|
"storageViewRouters": "Top urządzenia",
|
||||||
"storageViewRoutersHint": "Urządzenia z największą liczbą snapshotów w repozytorium.",
|
"storageViewRoutersHint": "Urządzenia z największą liczbą snapshotów w repozytorium.",
|
||||||
"storageChartEmpty": "Brak danych do narysowania wykresu.",
|
"storageChartEmpty": "Brak danych do narysowania wykresu.",
|
||||||
"storageSnapshotTitle": "Metryki repozytorium",
|
"storageSnapshotTitle": "Metryki repozytorium",
|
||||||
@@ -170,12 +171,12 @@
|
|||||||
"optionalPassword": "Opcjonalne hasło",
|
"optionalPassword": "Opcjonalne hasło",
|
||||||
"optionalPrivateKey": "Opcjonalny klucz prywatny",
|
"optionalPrivateKey": "Opcjonalny klucz prywatny",
|
||||||
"saveRouter": "Zapisz urządzenie",
|
"saveRouter": "Zapisz urządzenie",
|
||||||
"profileEyebrow": "profil routera",
|
"profileEyebrow": "profil urządzenia",
|
||||||
"detailSubtitle": "Operacje urządzenia i historia backupów",
|
"detailSubtitle": "Operacje urządzenia i historia backupów",
|
||||||
"exportOne": "Eksport",
|
"exportOne": "Eksport",
|
||||||
"binaryOne": "Backup",
|
"binaryOne": "Backup",
|
||||||
"testConnection": "Test połączenia",
|
"testConnection": "Test połączenia",
|
||||||
"deleteRouter": "Usuń router",
|
"deleteRouter": "Usuń urządzenie",
|
||||||
"exportsLabel": "Eksporty",
|
"exportsLabel": "Eksporty",
|
||||||
"exportsLabelHint": "Tekstowe snapshoty",
|
"exportsLabelHint": "Tekstowe snapshoty",
|
||||||
"binaryLabel": "Backupy binarne",
|
"binaryLabel": "Backupy binarne",
|
||||||
@@ -232,7 +233,25 @@
|
|||||||
"connectionSectionHint": "Podstawowa tożsamość urządzenia i endpoint używany do połączenia.",
|
"connectionSectionHint": "Podstawowa tożsamość urządzenia i endpoint używany do połączenia.",
|
||||||
"credentialsSectionTitle": "Dostęp i poświadczenia",
|
"credentialsSectionTitle": "Dostęp i poświadczenia",
|
||||||
"routerDialogSubtitle": "Ustaw adres urządzenia, dane dostępu SSH i preferowaną metodę logowania.",
|
"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ń."
|
"switchDialogSubtitle": "Ustaw endpoint SwitchOS i opcjonalne poświadczenia lokalne lub domyślne z ustawień.",
|
||||||
|
"backupPolicy": "Kopie",
|
||||||
|
"backupSettingsTitle": "Ustawienia kopii",
|
||||||
|
"backupSettingsHint": "Steruj osobno eksportem, backupem binarnym i pingiem dla tego urządzenia.",
|
||||||
|
"deleteDevice": "Usuń urządzenie",
|
||||||
|
"disableAllBackupsHint": "Jednym przełącznikiem blokuje wszystkie typy kopii i automatycznie zaznacza opcje poniżej.",
|
||||||
|
"disableExportsHint": "Wyłącza tylko eksporty tekstowe i zostawia backup binarny bez zmian.",
|
||||||
|
"disableBinaryBackupsHint": "Wyłącza tylko backupy binarne i nie rusza eksportów tekstowych.",
|
||||||
|
"disablePing": "Wyłącz ping do urządzenia",
|
||||||
|
"disablePingHint": "Wyłącza sprawdzanie dostępności pingiem na liście urządzeń.",
|
||||||
|
"pingDisabled": "Ping wyłączony",
|
||||||
|
"disableAllBackups": "Wyłącz wszystkie kopie dla tego urządzenia",
|
||||||
|
"disableExports": "Wyłącz eksporty",
|
||||||
|
"disableBinaryBackups": "Wyłącz kopie binarne",
|
||||||
|
"backupsDisabledAll": "Wszystkie kopie wyłączone",
|
||||||
|
"ping": "Ping",
|
||||||
|
"pingChecking": "Sprawdzanie...",
|
||||||
|
"noPing": "Brak pingu",
|
||||||
|
"pingAvailable": "Ping dostępny"
|
||||||
},
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"title": "Repozytorium",
|
"title": "Repozytorium",
|
||||||
@@ -250,9 +269,9 @@
|
|||||||
"binaryCard": "Backupy binarne",
|
"binaryCard": "Backupy binarne",
|
||||||
"binaryHint": "Obrazy odzyskiwania",
|
"binaryHint": "Obrazy odzyskiwania",
|
||||||
"filtersTitle": "Filtry",
|
"filtersTitle": "Filtry",
|
||||||
"filtersSubtitle": "Zawęź listę plików po routerze, typie lub słowie kluczowym.",
|
"filtersSubtitle": "Zawęź listę plików po urządzeniu, typie lub słowie kluczowym.",
|
||||||
"searchLabel": "Szukaj",
|
"searchLabel": "Szukaj",
|
||||||
"searchPlaceholder": "Szukaj po pliku lub routerze",
|
"searchPlaceholder": "Szukaj po pliku lub urządzeniu",
|
||||||
"typeLabel": "Typ",
|
"typeLabel": "Typ",
|
||||||
"routerLabel": "Urządzenie",
|
"routerLabel": "Urządzenie",
|
||||||
"dateLabel": "Data",
|
"dateLabel": "Data",
|
||||||
@@ -289,7 +308,7 @@
|
|||||||
"compareLatestPair": "Najnowsza para",
|
"compareLatestPair": "Najnowsza para",
|
||||||
"setOlder": "Ustaw jako starszy",
|
"setOlder": "Ustaw jako starszy",
|
||||||
"setNewer": "Ustaw jako nowszy",
|
"setNewer": "Ustaw jako nowszy",
|
||||||
"latestForRouter": "Diff dla routera",
|
"latestForRouter": "Diff dla urządzenia",
|
||||||
"binaryNoCompare": "Diff tylko dla .rsc",
|
"binaryNoCompare": "Diff tylko dla .rsc",
|
||||||
"openPlainDiff": "Pokaż diff tekstowy",
|
"openPlainDiff": "Pokaż diff tekstowy",
|
||||||
"minutesAgo": "{{value}} min temu",
|
"minutesAgo": "{{value}} min temu",
|
||||||
@@ -299,8 +318,8 @@
|
|||||||
"compareSubtitle": "Wybierz dwa pliki .rsc i uruchom diff bez przewijania całej tabeli.",
|
"compareSubtitle": "Wybierz dwa pliki .rsc i uruchom diff bez przewijania całej tabeli.",
|
||||||
"exportPoolLabel": "eksportów gotowych do porównania",
|
"exportPoolLabel": "eksportów gotowych do porównania",
|
||||||
"compareSelectionHint": "Wybierz starszy i nowszy plik",
|
"compareSelectionHint": "Wybierz starszy i nowszy plik",
|
||||||
"compareReadySameRouter": "Para gotowa · router {{router}}",
|
"compareReadySameRouter": "Para gotowa · urządzenie {{router}}",
|
||||||
"compareReadyMixedRouters": "Para gotowa · różne routery"
|
"compareReadyMixedRouters": "Para gotowa · różne urządzenia"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Ustawienia",
|
"title": "Ustawienia",
|
||||||
@@ -431,16 +450,16 @@
|
|||||||
"error": "Błąd",
|
"error": "Błąd",
|
||||||
"exportPreviewLoaded": "Załadowano podgląd eksportu.",
|
"exportPreviewLoaded": "Załadowano podgląd eksportu.",
|
||||||
"backupSentEmail": "Backup został wysłany e-mailem.",
|
"backupSentEmail": "Backup został wysłany e-mailem.",
|
||||||
"binaryUploaded": "Backup binarny został wysłany na router.",
|
"binaryUploaded": "Backup binarny został wysłany na urządzenie.",
|
||||||
"backupDeleted": "Backup został usunięty.",
|
"backupDeleted": "Backup został usunięty.",
|
||||||
"selectedBackupsDeleted": "Wybrane backupy zostały usunięte.",
|
"selectedBackupsDeleted": "Wybrane backupy zostały usunięte.",
|
||||||
"diffLoaded": "Załadowano diff.",
|
"diffLoaded": "Załadowano diff.",
|
||||||
"archivePrepared": "Archiwum zostało przygotowane.",
|
"archivePrepared": "Archiwum zostało przygotowane.",
|
||||||
"exportedRouters": "Wykonano eksport dla {{count}} urządzeń.",
|
"exportedRouters": "Wykonano eksport dla {{count}} urządzeń.",
|
||||||
"binaryCompletedRouters": "Wykonano backup binarny dla {{count}} urządzeń.",
|
"binaryCompletedRouters": "Wykonano backup binarny dla {{count}} urządzeń.",
|
||||||
"routerCreated": "Router został dodany.",
|
"routerCreated": "Urządzenie zostało dodane.",
|
||||||
"routerUpdated": "Router został zaktualizowany.",
|
"routerUpdated": "Urządzenie zostało zaktualizowane.",
|
||||||
"routerDeleted": "Router został usunięty.",
|
"routerDeleted": "Urządzenie zostało usunięte.",
|
||||||
"exportCreated": "Eksport został utworzony.",
|
"exportCreated": "Eksport został utworzony.",
|
||||||
"binaryCreated": "Backup binarny został utworzony.",
|
"binaryCreated": "Backup binarny został utworzony.",
|
||||||
"connectionSuccessful": "Połączenie zakończone powodzeniem.",
|
"connectionSuccessful": "Połączenie zakończone powodzeniem.",
|
||||||
@@ -463,7 +482,7 @@
|
|||||||
"header": "Potwierdzenie",
|
"header": "Potwierdzenie",
|
||||||
"deleteBackup": "Usunąć ten plik backupu?",
|
"deleteBackup": "Usunąć ten plik backupu?",
|
||||||
"deleteSelectedFiles": "Usunąć {{count}} zaznaczonych plików?",
|
"deleteSelectedFiles": "Usunąć {{count}} zaznaczonych plików?",
|
||||||
"deleteRouterWithFiles": "Usunąć router i wszystkie powiązane pliki?",
|
"deleteRouterWithFiles": "Usunąć urządzenie i wszystkie powiązane pliki?",
|
||||||
"deleteLogsOlderThan": "Usunąć logi starsze niż {{days}} dni?"
|
"deleteLogsOlderThan": "Usunąć logi starsze niż {{days}} dni?"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
@@ -485,7 +504,7 @@
|
|||||||
"exportsCard": "Eksporty do diffu",
|
"exportsCard": "Eksporty do diffu",
|
||||||
"exportsCardHint": "Pliki .rsc w bieżącym zakresie",
|
"exportsCardHint": "Pliki .rsc w bieżącym zakresie",
|
||||||
"scopeCard": "Zakres",
|
"scopeCard": "Zakres",
|
||||||
"scopeCardHint": "Wybrany router lub cała flota",
|
"scopeCardHint": "Wybrane urządzenie lub cała flota",
|
||||||
"scopeTag": "Zakres",
|
"scopeTag": "Zakres",
|
||||||
"readyCard": "Para",
|
"readyCard": "Para",
|
||||||
"readyCardHint": "Stan wyboru do porównania",
|
"readyCardHint": "Stan wyboru do porównania",
|
||||||
@@ -494,7 +513,7 @@
|
|||||||
"lastDiffCardHint": "Ostatnio otwarta para plików",
|
"lastDiffCardHint": "Ostatnio otwarta para plików",
|
||||||
"lastDiffTag": "Historia",
|
"lastDiffTag": "Historia",
|
||||||
"workspaceTitle": "Stanowisko porównawcze",
|
"workspaceTitle": "Stanowisko porównawcze",
|
||||||
"workspaceSubtitle": "Wybierz router, ustaw starszy i nowszy eksport, a potem otwórz diff w modalu.",
|
"workspaceSubtitle": "Wybierz urządzenie, ustaw starszy i nowszy eksport, a potem otwórz diff w modalu.",
|
||||||
"tableTitle": "Eksporty do wyboru",
|
"tableTitle": "Eksporty do wyboru",
|
||||||
"tableSubtitle": "Szybkie przypisanie starszego i nowszego pliku oraz podgląd bez opuszczania strony.",
|
"tableSubtitle": "Szybkie przypisanie starszego i nowszego pliku oraz podgląd bez opuszczania strony.",
|
||||||
"waitingTag": "Czeka",
|
"waitingTag": "Czeka",
|
||||||
|
|||||||
@@ -2382,12 +2382,15 @@ app-page-header{
|
|||||||
}
|
}
|
||||||
|
|
||||||
.router-detail-grid--inspection{
|
.router-detail-grid--inspection{
|
||||||
|
grid-template-columns: minmax(0, 1.2fr) minmax(340px, 0.9fr);
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.router-detail-inspection-stack{
|
.router-detail-split-grid{
|
||||||
display: grid;
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.router-detail-grid--stack{
|
.router-detail-grid--stack{
|
||||||
@@ -2510,7 +2513,7 @@ app-page-header{
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 980px) {
|
@media (max-width: 980px) {
|
||||||
.diff-workspace__pair, .router-detail-grid--inspection{
|
.diff-workspace__pair, .router-detail-grid--inspection, .router-detail-split-grid{
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3432,3 +3435,35 @@ body.dark-theme .p-toast .p-toast-summary, body.dark-theme .p-toast .p-toast-det
|
|||||||
height: 2rem;
|
height: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.device-settings-form{display:block;}
|
||||||
|
.device-settings-stack{display:grid;gap:12px;}
|
||||||
|
.device-settings-actions{margin-top:16px;}
|
||||||
|
.device-toggle{position:relative;display:grid;grid-template-columns:auto auto auto minmax(0,1fr) auto;align-items:center;gap:14px;padding:16px 18px;border:1px solid color-mix(in srgb,var(--border-color) 88%, transparent);border-radius:18px;background:linear-gradient(135deg,color-mix(in srgb,var(--surface-1) 92%, transparent),color-mix(in srgb,var(--surface-2) 90%, transparent));cursor:pointer;transition:border-color .15s ease,transform .15s ease,background .15s ease,box-shadow .15s ease;box-shadow:var(--shadow-md);overflow:hidden;}
|
||||||
|
.device-toggle::after{content:"";position:absolute;inset:0;pointer-events:none;background:linear-gradient(90deg,transparent,rgba(255,255,255,.04),transparent);opacity:0;transition:opacity .15s ease;}
|
||||||
|
.device-toggle:hover{border-color:color-mix(in srgb,var(--accent) 55%, var(--border-color));transform:translateY(-1px);}
|
||||||
|
.device-toggle:hover::after,.device-toggle.is-active::after{opacity:1;}
|
||||||
|
.device-toggle.is-active{border-color:color-mix(in srgb,var(--accent) 60%, var(--border-color));background:linear-gradient(135deg,color-mix(in srgb,var(--accent) 12%, var(--surface-1)),color-mix(in srgb,var(--accent) 6%, var(--surface-2)));box-shadow:0 16px 40px -26px color-mix(in srgb,var(--accent) 45%, transparent),var(--shadow-md);}
|
||||||
|
.device-toggle input{position:absolute;opacity:0;pointer-events:none;inline-size:1px;block-size:1px;}
|
||||||
|
.device-toggle__switch{position:relative;display:inline-flex;align-items:center;inline-size:46px;block-size:26px;border-radius:999px;background:color-mix(in srgb,var(--surface-3) 88%, transparent);border:1px solid color-mix(in srgb,var(--border-color) 82%, transparent);box-shadow:inset 0 1px 2px rgba(0,0,0,.16);transition:background .15s ease,border-color .15s ease,box-shadow .15s ease;}
|
||||||
|
.device-toggle__switch::after{content:"";position:absolute;inset:3px auto 3px 3px;inline-size:18px;block-size:18px;border-radius:50%;background:var(--text-main);box-shadow:0 4px 10px rgba(0,0,0,.18);transition:transform .15s ease,background .15s ease;}
|
||||||
|
.device-toggle__icon{display:grid;place-items:center;inline-size:40px;block-size:40px;border-radius:12px;background:color-mix(in srgb,var(--surface-3) 84%, transparent);border:1px solid color-mix(in srgb,var(--border-color) 82%, transparent);color:var(--text-muted);font-size:1rem;}
|
||||||
|
.device-toggle.is-active .device-toggle__icon{color:var(--accent);border-color:color-mix(in srgb,var(--accent) 35%, var(--border-color));background:color-mix(in srgb,var(--accent) 12%, var(--surface-3));}
|
||||||
|
.device-toggle.is-active .device-toggle__switch{background:color-mix(in srgb,var(--accent) 20%, var(--surface-3));border-color:color-mix(in srgb,var(--accent) 35%, var(--border-color));box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 20%, transparent);}
|
||||||
|
.device-toggle.is-active .device-toggle__switch::after{transform:translateX(20px);background:var(--accent);}
|
||||||
|
.device-toggle__content{display:grid;gap:4px;min-width:0;}
|
||||||
|
.device-toggle__content strong{font-size:.94rem;color:var(--text-main);}
|
||||||
|
.device-toggle__content small{line-height:1.45;color:var(--text-muted);}
|
||||||
|
.device-toggle__state{font-size:.75rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--text-muted);padding:8px 10px;border-radius:999px;background:color-mix(in srgb,var(--surface-3) 88%, transparent);border:1px solid color-mix(in srgb,var(--border-color) 82%, transparent);}
|
||||||
|
.device-toggle.is-active .device-toggle__state{color:var(--accent);border-color:color-mix(in srgb,var(--accent) 28%, var(--border-color));background:color-mix(in srgb,var(--accent) 12%, var(--surface-3));}
|
||||||
|
body.dark-theme .device-toggle{background:linear-gradient(135deg,color-mix(in srgb,var(--surface-2) 94%, transparent),color-mix(in srgb,var(--surface-1) 88%, transparent));}
|
||||||
|
body.dark-theme .device-toggle.is-active{background:linear-gradient(135deg,color-mix(in srgb,var(--accent) 14%, var(--surface-2)),color-mix(in srgb,var(--accent) 7%, var(--surface-1)));}
|
||||||
|
|
||||||
|
@media (max-width: 1100px){
|
||||||
|
.router-detail-split-grid{grid-template-columns:minmax(0,1fr);}
|
||||||
|
}
|
||||||
|
@media (max-width: 720px){
|
||||||
|
.device-toggle{grid-template-columns:auto auto auto minmax(0,1fr);align-items:start;}
|
||||||
|
.device-toggle__state{grid-column:2 / -1;justify-self:start;}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user