switchos support
This commit is contained in:
@@ -1,14 +1,12 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.routes import auth, backups, dashboard, health, logs, routers, settings, swos_beta
|
||||
from app.api.routes import auth, backups, dashboard, health, logs, routers, settings
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
||||
api_router.include_router(dashboard.router, prefix="/dashboard", tags=["dashboard"])
|
||||
api_router.include_router(routers.router, prefix="/routers", tags=["routers"])
|
||||
api_router.include_router(backups.router, prefix="/backups", tags=["backups"])
|
||||
api_router.include_router(settings.router, prefix="/settings", tags=["settings"])
|
||||
api_router.include_router(logs.router, prefix="/logs", tags=["logs"])
|
||||
api_router.include_router(health.router, tags=["health"])
|
||||
|
||||
api_router.include_router(swos_beta.router, prefix='/swos-beta', tags=['swos-beta'])
|
||||
api_router.include_router(auth.router, prefix='/auth', tags=['auth'])
|
||||
api_router.include_router(dashboard.router, prefix='/dashboard', tags=['dashboard'])
|
||||
api_router.include_router(routers.router, prefix='/routers', tags=['routers'])
|
||||
api_router.include_router(backups.router, prefix='/backups', tags=['backups'])
|
||||
api_router.include_router(settings.router, prefix='/settings', tags=['settings'])
|
||||
api_router.include_router(logs.router, prefix='/logs', tags=['logs'])
|
||||
api_router.include_router(health.router, tags=['health'])
|
||||
|
||||
@@ -13,73 +13,108 @@ from app.services.settings_service import settings_service
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def serialize_router(router: Router, global_ssh_key: str | None = None) -> RouterResponse:
|
||||
def serialize_router(router: Router, global_settings) -> RouterResponse:
|
||||
has_router_key = bool((router.ssh_key or '').strip())
|
||||
has_global_key = bool((global_ssh_key or '').strip())
|
||||
has_global_key = bool((global_settings.global_ssh_key or '').strip())
|
||||
router_user = (router.ssh_user or '').strip() or None
|
||||
router_password = (router.ssh_password or '').strip() or None
|
||||
default_swos_user = (global_settings.default_switchos_username or '').strip() or None
|
||||
default_swos_password = (global_settings.default_switchos_password or '').strip() or None
|
||||
effective_username = router_user
|
||||
uses_global_switchos_credentials = False
|
||||
has_effective_password = bool(router_password)
|
||||
|
||||
if router.device_type == 'switchos':
|
||||
effective_username = router_user or default_swos_user
|
||||
uses_global_switchos_credentials = bool(
|
||||
(not router_user and default_swos_user) or (not router_password and default_swos_password)
|
||||
)
|
||||
has_effective_password = bool(router_password or default_swos_password)
|
||||
|
||||
payload = RouterResponse.model_validate(router, from_attributes=True).model_dump()
|
||||
payload['uses_global_ssh_key'] = has_global_key and not has_router_key
|
||||
payload['has_effective_ssh_key'] = has_router_key or has_global_key
|
||||
payload['effective_username'] = effective_username
|
||||
payload['uses_global_ssh_key'] = router.device_type == 'routeros' and has_global_key and not has_router_key
|
||||
payload['has_effective_ssh_key'] = router.device_type == 'routeros' and (has_router_key or has_global_key)
|
||||
payload['uses_global_switchos_credentials'] = uses_global_switchos_credentials
|
||||
payload['has_effective_password'] = has_effective_password
|
||||
payload['supports_export'] = router.device_type == 'routeros'
|
||||
payload['supports_restore_upload'] = router.device_type == 'routeros'
|
||||
return RouterResponse.model_validate(payload)
|
||||
|
||||
|
||||
@router.get("", response_model=list[RouterResponse])
|
||||
@router.get('', response_model=list[RouterResponse])
|
||||
def list_routers(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||
settings = settings_service.get_or_create(db)
|
||||
global_settings = settings_service.get_or_create(db)
|
||||
routers = db.query(Router).filter(Router.owner_id == current_user.id).order_by(Router.created_at.desc()).all()
|
||||
return [serialize_router(router, settings.global_ssh_key) for router in routers]
|
||||
return [serialize_router(router, global_settings) for router in 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)):
|
||||
router = Router(**payload.model_dump(), owner_id=current_user.id)
|
||||
router_data = payload.model_dump()
|
||||
if router_data.get('device_type') == 'switchos' and router_data.get('ssh_user') is None:
|
||||
router_data['ssh_user'] = ''
|
||||
router = Router(**router_data, owner_id=current_user.id)
|
||||
db.add(router)
|
||||
db.commit()
|
||||
db.refresh(router)
|
||||
settings = settings_service.get_or_create(db)
|
||||
return serialize_router(router, settings.global_ssh_key)
|
||||
global_settings = settings_service.get_or_create(db)
|
||||
return serialize_router(router, global_settings)
|
||||
|
||||
|
||||
@router.get("/{router_id}", response_model=RouterResponse)
|
||||
@router.get('/{router_id}', response_model=RouterResponse)
|
||||
def get_router(router_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||
router = db.query(Router).filter(Router.id == router_id, Router.owner_id == current_user.id).first()
|
||||
if not router:
|
||||
raise HTTPException(status_code=404, detail="Router not found")
|
||||
settings = settings_service.get_or_create(db)
|
||||
return serialize_router(router, settings.global_ssh_key)
|
||||
raise HTTPException(status_code=404, detail='Device not found')
|
||||
global_settings = settings_service.get_or_create(db)
|
||||
return serialize_router(router, global_settings)
|
||||
|
||||
|
||||
@router.put("/{router_id}", response_model=RouterResponse)
|
||||
@router.put('/{router_id}', response_model=RouterResponse)
|
||||
def update_router(router_id: int, payload: RouterUpdate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||
router = db.query(Router).filter(Router.id == router_id, Router.owner_id == current_user.id).first()
|
||||
if not router:
|
||||
raise HTTPException(status_code=404, detail="Router not found")
|
||||
for key, value in payload.model_dump(exclude_unset=True).items():
|
||||
raise HTTPException(status_code=404, detail='Device not found')
|
||||
changes = payload.model_dump(exclude_unset=True)
|
||||
target_device_type = changes.get('device_type', router.device_type)
|
||||
if target_device_type == 'switchos':
|
||||
changes['ssh_key'] = None
|
||||
if 'port' not in changes:
|
||||
changes['port'] = 80
|
||||
if changes.get('ssh_user') is None:
|
||||
changes['ssh_user'] = ''
|
||||
elif target_device_type == 'routeros' and 'port' not in changes and router.device_type != 'routeros':
|
||||
changes['port'] = 22
|
||||
if not changes.get('ssh_user'):
|
||||
changes['ssh_user'] = router.ssh_user or 'admin'
|
||||
for key, value in changes.items():
|
||||
setattr(router, key, value)
|
||||
db.add(router)
|
||||
db.commit()
|
||||
db.refresh(router)
|
||||
settings = settings_service.get_or_create(db)
|
||||
return serialize_router(router, settings.global_ssh_key)
|
||||
global_settings = settings_service.get_or_create(db)
|
||||
return serialize_router(router, global_settings)
|
||||
|
||||
|
||||
@router.delete("/{router_id}")
|
||||
@router.delete('/{router_id}')
|
||||
def delete_router(router_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||
router = db.query(Router).filter(Router.id == router_id, Router.owner_id == current_user.id).first()
|
||||
if not router:
|
||||
raise HTTPException(status_code=404, detail="Router not found")
|
||||
raise HTTPException(status_code=404, detail='Device not found')
|
||||
for backup in list(router.backups):
|
||||
path = Path(backup.file_path)
|
||||
if path.exists():
|
||||
path.unlink()
|
||||
db.delete(router)
|
||||
db.commit()
|
||||
return {"message": "Router deleted"}
|
||||
return {'message': 'Device deleted'}
|
||||
|
||||
|
||||
@router.get("/{router_id}/test-connection", response_model=RouterTestConnection)
|
||||
@router.get('/{router_id}/test-connection', response_model=RouterTestConnection)
|
||||
def test_connection(router_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||
router = db.query(Router).filter(Router.id == router_id, Router.owner_id == current_user.id).first()
|
||||
if not router:
|
||||
raise HTTPException(status_code=404, detail="Router not found")
|
||||
settings = settings_service.get_or_create(db)
|
||||
return router_service.test_connection(db, router, settings.global_ssh_key)
|
||||
raise HTTPException(status_code=404, detail='Device not found')
|
||||
global_settings = settings_service.get_or_create(db)
|
||||
return router_service.test_connection(db, router, global_settings)
|
||||
|
||||
@@ -23,6 +23,9 @@ def serialize_settings(settings: GlobalSettings) -> SettingsResponse:
|
||||
payload = SettingsResponse.model_validate(settings, from_attributes=True).model_dump()
|
||||
payload['global_ssh_key'] = None
|
||||
payload['has_global_ssh_key'] = bool((settings.global_ssh_key or '').strip())
|
||||
payload['has_default_switchos_credentials'] = bool(
|
||||
(settings.default_switchos_username or '').strip() or (settings.default_switchos_password or '').strip()
|
||||
)
|
||||
return SettingsResponse.model_validate(payload)
|
||||
|
||||
|
||||
|
||||
@@ -42,10 +42,13 @@ def _run_lightweight_migrations() -> None:
|
||||
tables = set(inspect(engine).get_table_names())
|
||||
if 'global_settings' in tables:
|
||||
_ensure_column('global_settings', 'connection_test_interval_minutes', 'INTEGER DEFAULT 0')
|
||||
_ensure_column('global_settings', 'default_switchos_username', 'VARCHAR(120)')
|
||||
_ensure_column('global_settings', 'default_switchos_password', 'VARCHAR(255)')
|
||||
if 'users' in tables:
|
||||
_ensure_column('users', 'preferred_language', "VARCHAR(8) DEFAULT 'pl' NOT NULL")
|
||||
_ensure_column('users', 'preferred_font', "VARCHAR(32) DEFAULT 'default' NOT NULL")
|
||||
if 'routers' in tables:
|
||||
_ensure_column('routers', 'device_type', "VARCHAR(32) DEFAULT 'routeros' NOT NULL")
|
||||
_ensure_column('routers', 'last_connection_status', 'BOOLEAN')
|
||||
_ensure_column('routers', 'last_connection_tested_at', 'DATETIME')
|
||||
_ensure_column('routers', 'last_connection_error', 'TEXT')
|
||||
@@ -53,6 +56,11 @@ def _run_lightweight_migrations() -> None:
|
||||
_ensure_column('routers', 'last_connection_model', 'VARCHAR(255)')
|
||||
_ensure_column('routers', 'last_connection_version', 'VARCHAR(255)')
|
||||
_ensure_column('routers', 'last_connection_uptime', 'VARCHAR(255)')
|
||||
_ensure_column('routers', 'last_connection_transport', 'VARCHAR(32)')
|
||||
_ensure_column('routers', 'last_connection_server', 'VARCHAR(255)')
|
||||
_ensure_column('routers', 'last_connection_auth_mode', 'VARCHAR(64)')
|
||||
_ensure_column('routers', 'last_connection_http_status', 'VARCHAR(32)')
|
||||
_ensure_column('routers', 'last_connection_backup_available', 'BOOLEAN')
|
||||
|
||||
|
||||
def init_db():
|
||||
|
||||
@@ -11,6 +11,7 @@ class Router(Base):
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
owner_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||||
name = Column(String(120), nullable=False)
|
||||
device_type = Column(String(32), nullable=False, default="routeros")
|
||||
host = Column(String(255), nullable=False)
|
||||
port = Column(Integer, nullable=False, default=22)
|
||||
ssh_user = Column(String(120), nullable=False, default="admin")
|
||||
@@ -23,6 +24,11 @@ class Router(Base):
|
||||
last_connection_model = Column(String(255), nullable=True)
|
||||
last_connection_version = Column(String(255), nullable=True)
|
||||
last_connection_uptime = Column(String(255), nullable=True)
|
||||
last_connection_transport = Column(String(32), nullable=True)
|
||||
last_connection_server = Column(String(255), nullable=True)
|
||||
last_connection_auth_mode = Column(String(64), nullable=True)
|
||||
last_connection_http_status = Column(String(32), nullable=True)
|
||||
last_connection_backup_available = Column(Boolean, nullable=True)
|
||||
created_at = Column(DateTime, server_default=func.now(), nullable=False)
|
||||
|
||||
backups = relationship("Backup", back_populates="router", cascade="all, delete-orphan")
|
||||
|
||||
@@ -15,6 +15,8 @@ class GlobalSettings(Base):
|
||||
enable_auto_export = Column(Boolean, default=False)
|
||||
connection_test_interval_minutes = Column(Integer, default=0)
|
||||
global_ssh_key = Column(Text, nullable=True)
|
||||
default_switchos_username = Column(String(120), nullable=True)
|
||||
default_switchos_password = Column(String(255), nullable=True)
|
||||
pushover_token = Column(String(255), nullable=True)
|
||||
pushover_userkey = Column(String(255), nullable=True)
|
||||
notify_failures_only = Column(Boolean, default=True)
|
||||
|
||||
@@ -8,6 +8,7 @@ class BackupResponse(BaseModel):
|
||||
id: int
|
||||
router_id: int
|
||||
router_name: str | None = None
|
||||
device_type: str = "routeros"
|
||||
file_path: str
|
||||
file_name: str
|
||||
backup_type: str
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from pydantic import BaseModel, Field, field_validator, model_validator
|
||||
|
||||
ALLOWED_NAME_REGEX = re.compile(r"^[A-Za-z0-9_-]+$")
|
||||
DeviceType = Literal["routeros", "switchos"]
|
||||
|
||||
|
||||
class RouterBase(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=120)
|
||||
device_type: DeviceType = "routeros"
|
||||
host: str = Field(min_length=1, max_length=255)
|
||||
port: int = Field(default=22, ge=1, le=65535)
|
||||
ssh_user: str = Field(default="admin", min_length=1, max_length=120)
|
||||
port: int | None = Field(default=None, ge=1, le=65535)
|
||||
ssh_user: str | None = Field(default=None, max_length=120)
|
||||
ssh_key: str | None = None
|
||||
ssh_password: str | None = None
|
||||
|
||||
@@ -21,6 +24,23 @@ class RouterBase(BaseModel):
|
||||
raise ValueError("Only letters, digits, dashes and underscores are allowed")
|
||||
return value
|
||||
|
||||
@field_validator("host", "ssh_user", "ssh_key", "ssh_password", mode="before")
|
||||
@classmethod
|
||||
def normalize_text(cls, value: str | None) -> str | None:
|
||||
normalized = (value or "").strip()
|
||||
return normalized or None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def apply_device_defaults(self):
|
||||
if self.device_type == "routeros":
|
||||
self.port = self.port or 22
|
||||
self.ssh_user = self.ssh_user or "admin"
|
||||
return self
|
||||
|
||||
self.port = self.port or 80
|
||||
self.ssh_key = None
|
||||
return self
|
||||
|
||||
|
||||
class RouterCreate(RouterBase):
|
||||
pass
|
||||
@@ -28,18 +48,30 @@ class RouterCreate(RouterBase):
|
||||
|
||||
class RouterUpdate(BaseModel):
|
||||
name: str | None = None
|
||||
device_type: DeviceType | None = None
|
||||
host: str | None = None
|
||||
port: int | None = Field(default=None, ge=1, le=65535)
|
||||
ssh_user: str | None = None
|
||||
ssh_key: str | None = None
|
||||
ssh_password: str | None = None
|
||||
|
||||
@field_validator("name", "host", "ssh_user", "ssh_key", "ssh_password", mode="before")
|
||||
@classmethod
|
||||
def normalize_text(cls, value: str | None) -> str | None:
|
||||
normalized = (value or "").strip()
|
||||
return normalized or None
|
||||
|
||||
|
||||
class RouterResponse(RouterBase):
|
||||
id: int
|
||||
owner_id: int
|
||||
effective_username: str | None = None
|
||||
uses_global_ssh_key: bool = False
|
||||
has_effective_ssh_key: bool = False
|
||||
uses_global_switchos_credentials: bool = False
|
||||
has_effective_password: bool = False
|
||||
supports_export: bool = False
|
||||
supports_restore_upload: bool = False
|
||||
last_connection_status: bool | None = None
|
||||
last_connection_tested_at: datetime | None = None
|
||||
last_connection_error: str | None = None
|
||||
@@ -47,6 +79,11 @@ class RouterResponse(RouterBase):
|
||||
last_connection_model: str | None = None
|
||||
last_connection_version: str | None = None
|
||||
last_connection_uptime: str | None = None
|
||||
last_connection_transport: str | None = None
|
||||
last_connection_server: str | None = None
|
||||
last_connection_auth_mode: str | None = None
|
||||
last_connection_http_status: str | None = None
|
||||
last_connection_backup_available: bool | None = None
|
||||
created_at: datetime | None = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
@@ -60,3 +97,8 @@ class RouterTestConnection(BaseModel):
|
||||
hostname: str
|
||||
version: str | None = None
|
||||
error: str | None = None
|
||||
transport: str | None = None
|
||||
server: str | None = None
|
||||
auth_mode: str | None = None
|
||||
http_status: str | None = None
|
||||
backup_available: bool | None = None
|
||||
|
||||
@@ -15,6 +15,8 @@ class SettingsBase(BaseModel):
|
||||
enable_auto_export: bool = False
|
||||
connection_test_interval_minutes: int = Field(default=0, ge=0, le=1440)
|
||||
global_ssh_key: str | None = None
|
||||
default_switchos_username: str | None = None
|
||||
default_switchos_password: str | None = None
|
||||
pushover_token: str | None = None
|
||||
pushover_userkey: str | None = None
|
||||
notify_failures_only: bool = True
|
||||
@@ -30,9 +32,9 @@ class SettingsBase(BaseModel):
|
||||
def normalize_cron(cls, value: str | None) -> str:
|
||||
return (value or '').strip()
|
||||
|
||||
@field_validator('global_ssh_key', mode='before')
|
||||
@field_validator('global_ssh_key', 'default_switchos_username', 'default_switchos_password', mode='before')
|
||||
@classmethod
|
||||
def normalize_key(cls, value: str | None) -> str | None:
|
||||
def normalize_secret_text(cls, value: str | None) -> str | None:
|
||||
normalized = (value or '').strip()
|
||||
return normalized or None
|
||||
|
||||
@@ -55,6 +57,7 @@ class SettingsUpdate(SettingsBase):
|
||||
class SettingsResponse(SettingsBase):
|
||||
id: int
|
||||
has_global_ssh_key: bool = False
|
||||
has_default_switchos_credentials: bool = False
|
||||
|
||||
model_config = {'from_attributes': True}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class BackupService:
|
||||
def _router_for_user(self, db: Session, user: User, router_id: int) -> Router:
|
||||
router = db.query(Router).filter(Router.id == router_id, Router.owner_id == user.id).first()
|
||||
if not router:
|
||||
raise HTTPException(status_code=404, detail='Router not found')
|
||||
raise HTTPException(status_code=404, detail='Device not found')
|
||||
return router
|
||||
|
||||
def _serialize_backup(self, backup: Backup):
|
||||
@@ -28,6 +28,7 @@ class BackupService:
|
||||
'id': backup.id,
|
||||
'router_id': backup.router_id,
|
||||
'router_name': backup.router.name if backup.router else None,
|
||||
'device_type': backup.router.device_type if backup.router else 'routeros',
|
||||
'file_path': backup.file_path,
|
||||
'file_name': backup.file_name,
|
||||
'backup_type': backup.backup_type,
|
||||
@@ -179,6 +180,8 @@ class BackupService:
|
||||
|
||||
def export_router(self, db: Session, user: User, router_id: int) -> Backup:
|
||||
router = self._router_for_user(db, user, router_id)
|
||||
if router.device_type != 'routeros':
|
||||
raise HTTPException(status_code=400, detail='Text export is available only for RouterOS devices')
|
||||
settings = settings_service.get_or_create(db)
|
||||
stamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
name = f'{router.name}_{router.id}_{stamp}.rsc'
|
||||
@@ -190,12 +193,14 @@ class BackupService:
|
||||
db.add(backup)
|
||||
db.commit()
|
||||
db.refresh(backup)
|
||||
log_service.add(db, f'Export OK for router {router.name}')
|
||||
log_service.add(db, f'Export OK for device {router.name}')
|
||||
notification_service.notify(settings, f'Export {router.name} OK', True)
|
||||
return backup
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as exc:
|
||||
notification_service.notify(settings, f'Export {router.name} FAIL: {exc}', False)
|
||||
log_service.add(db, f'Export FAILED for router {router.name}: {exc}')
|
||||
log_service.add(db, f'Export FAILED for device {router.name}: {exc}')
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
||||
def binary_backup(self, db: Session, user: User, router_id: int) -> Backup:
|
||||
@@ -203,34 +208,41 @@ class BackupService:
|
||||
settings = settings_service.get_or_create(db)
|
||||
stamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
base_name = f'{router.name}_{router.id}_{stamp}'
|
||||
name = f'{base_name}.backup'
|
||||
extension = '.swb' if router.device_type == 'switchos' else '.backup'
|
||||
name = f'{base_name}{extension}'
|
||||
file_path = ensure_data_dir() / name
|
||||
try:
|
||||
router_service.binary_backup(router, base_name, str(file_path), settings.global_ssh_key)
|
||||
router_service.binary_backup(router, base_name, str(file_path), settings.global_ssh_key, settings)
|
||||
checksum = compute_checksum(str(file_path))
|
||||
backup = Backup(router_id=router.id, file_path=str(file_path), file_name=name, backup_type='binary', checksum=checksum)
|
||||
db.add(backup)
|
||||
db.commit()
|
||||
db.refresh(backup)
|
||||
log_service.add(db, f'Binary backup OK for router {router.name}')
|
||||
log_service.add(db, f'Binary backup OK for device {router.name}')
|
||||
notification_service.notify(settings, f'Backup {router.name} OK', True)
|
||||
return backup
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as exc:
|
||||
notification_service.notify(settings, f'Backup {router.name} FAIL: {exc}', False)
|
||||
log_service.add(db, f'Binary backup FAILED for router {router.name}: {exc}')
|
||||
log_service.add(db, f'Binary backup FAILED for device {router.name}: {exc}')
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
||||
def upload_backup_to_router(self, db: Session, user: User, router_id: int, backup_id: int):
|
||||
router = self._router_for_user(db, user, router_id)
|
||||
if router.device_type != 'routeros':
|
||||
raise HTTPException(status_code=400, detail='Restore upload is available only for RouterOS devices')
|
||||
backup = self.get_backup_for_user(db, user, backup_id)
|
||||
if backup.backup_type != 'binary':
|
||||
raise HTTPException(status_code=400, detail='Only binary backups can be uploaded')
|
||||
if backup.router and backup.router.device_type != 'routeros':
|
||||
raise HTTPException(status_code=400, detail='SwitchOS backup files cannot be restored over SSH upload')
|
||||
checksum = compute_checksum(backup.file_path)
|
||||
if backup.checksum and checksum != backup.checksum:
|
||||
raise HTTPException(status_code=400, detail='Checksum mismatch')
|
||||
settings = settings_service.get_or_create(db)
|
||||
router_service.upload_backup(router, backup.file_path, settings.global_ssh_key)
|
||||
log_service.add(db, f'Upload backup OK for router {router.name}')
|
||||
log_service.add(db, f'Upload backup OK for device {router.name}')
|
||||
|
||||
def delete_backup(self, db: Session, user: User, backup_id: int, commit: bool = True):
|
||||
backup = self.get_backup_for_user(db, user, backup_id)
|
||||
@@ -274,9 +286,10 @@ class BackupService:
|
||||
def email_backup(self, db: Session, user: User, backup_id: int):
|
||||
backup = self.get_backup_for_user(db, user, backup_id)
|
||||
settings = settings_service.get_or_create(db)
|
||||
platform_name = 'SwitchOS' if backup.router and backup.router.device_type == 'switchos' else 'RouterOS'
|
||||
noun = 'Export' if backup.backup_type == 'export' else 'Backup'
|
||||
subject = f'RouterOS {noun}: {backup.file_name}'
|
||||
body = f'Sending {backup.file_name} from router {backup.router.name}.'
|
||||
subject = f'{platform_name} {noun}: {backup.file_name}'
|
||||
body = f'Sending {backup.file_name} from device {backup.router.name}.'
|
||||
notification_service.send_email(settings, subject, body, backup.file_path)
|
||||
log_service.add(db, f'Email sent for backup {backup.file_name}')
|
||||
|
||||
@@ -284,6 +297,9 @@ 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'})
|
||||
continue
|
||||
try:
|
||||
backup = self.export_router(db, user, router.id)
|
||||
result.append({'router': router.name, 'status': 'ok', 'backup_id': backup.id})
|
||||
|
||||
@@ -6,6 +6,7 @@ import paramiko
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.router import Router
|
||||
from app.services.swos_beta_service import swos_beta_service
|
||||
|
||||
|
||||
class RouterService:
|
||||
@@ -47,18 +48,25 @@ class RouterService:
|
||||
return client
|
||||
|
||||
def export(self, router: Router, global_ssh_key: str | None = None) -> str:
|
||||
if router.device_type != 'routeros':
|
||||
raise ValueError('Export tekstowy jest dostępny tylko dla RouterOS.')
|
||||
client = self._connect(router, global_ssh_key)
|
||||
_, stdout, _ = client.exec_command("/export")
|
||||
output = stdout.read().decode("utf-8", errors="ignore")
|
||||
_, stdout, _ = client.exec_command('/export')
|
||||
output = stdout.read().decode('utf-8', errors='ignore')
|
||||
client.close()
|
||||
return output
|
||||
|
||||
def binary_backup(self, router: Router, backup_name: str, local_path: str, global_ssh_key: str | None = None) -> str:
|
||||
def binary_backup(self, router: Router, backup_name: str, local_path: str, global_ssh_key: str | None = None, global_settings=None) -> str:
|
||||
if router.device_type == 'switchos':
|
||||
downloaded = swos_beta_service.download_backup_for_router(router, global_settings)
|
||||
Path(local_path).write_bytes(downloaded.content)
|
||||
return local_path
|
||||
|
||||
client = self._connect(router, global_ssh_key)
|
||||
_, stdout, _ = client.exec_command(f"/system backup save name={backup_name}")
|
||||
_, stdout, _ = client.exec_command(f'/system backup save name={backup_name}')
|
||||
stdout.channel.recv_exit_status()
|
||||
sftp = client.open_sftp()
|
||||
remote_file = f"{backup_name}.backup"
|
||||
remote_file = f'{backup_name}.backup'
|
||||
sftp.get(remote_file, local_path)
|
||||
try:
|
||||
sftp.remove(remote_file)
|
||||
@@ -69,6 +77,8 @@ class RouterService:
|
||||
return local_path
|
||||
|
||||
def upload_backup(self, router: Router, local_backup_path: str, global_ssh_key: str | None = None):
|
||||
if router.device_type != 'routeros':
|
||||
raise ValueError('Przywracanie plików jest dostępne tylko dla RouterOS.')
|
||||
client = self._connect(router, global_ssh_key)
|
||||
sftp = client.open_sftp()
|
||||
target_name = Path(local_backup_path).name
|
||||
@@ -76,64 +86,84 @@ class RouterService:
|
||||
sftp.close()
|
||||
client.close()
|
||||
|
||||
def probe_connection(self, router: Router, global_ssh_key: str | None = None):
|
||||
def _probe_routeros_connection(self, router: Router, global_ssh_key: str | None = None):
|
||||
tested_at = datetime.utcnow()
|
||||
try:
|
||||
client = self._connect(router, global_ssh_key)
|
||||
_, stdout, _ = client.exec_command("/system resource print without-paging")
|
||||
resource_output = stdout.read().decode("utf-8", errors="ignore")
|
||||
_, stdout, _ = client.exec_command("/system identity print")
|
||||
identity_output = stdout.read().decode("utf-8", errors="ignore")
|
||||
_, stdout, _ = client.exec_command('/system resource print without-paging')
|
||||
resource_output = stdout.read().decode('utf-8', errors='ignore')
|
||||
_, stdout, _ = client.exec_command('/system identity print')
|
||||
identity_output = stdout.read().decode('utf-8', errors='ignore')
|
||||
client.close()
|
||||
model = "Unknown"
|
||||
uptime = "Unknown"
|
||||
hostname = "Unknown"
|
||||
version = "Unknown"
|
||||
model = 'Unknown'
|
||||
uptime = 'Unknown'
|
||||
hostname = 'Unknown'
|
||||
version = 'Unknown'
|
||||
for line in resource_output.splitlines():
|
||||
if "board-name" in line:
|
||||
model = line.split(":", 1)[1].strip()
|
||||
if "uptime" in line:
|
||||
uptime = line.split(":", 1)[1].strip()
|
||||
if "version" in line:
|
||||
version = line.split(":", 1)[1].strip()
|
||||
if 'board-name' in line:
|
||||
model = line.split(':', 1)[1].strip()
|
||||
if 'uptime' in line:
|
||||
uptime = line.split(':', 1)[1].strip()
|
||||
if 'version' in line:
|
||||
version = line.split(':', 1)[1].strip()
|
||||
for line in identity_output.splitlines():
|
||||
if "name" in line:
|
||||
hostname = line.split(":", 1)[1].strip()
|
||||
if 'name' in line:
|
||||
hostname = line.split(':', 1)[1].strip()
|
||||
return {
|
||||
"success": True,
|
||||
"tested_at": tested_at,
|
||||
"model": model,
|
||||
"uptime": uptime,
|
||||
"hostname": hostname,
|
||||
"version": version,
|
||||
"error": None,
|
||||
'success': True,
|
||||
'tested_at': tested_at,
|
||||
'model': model,
|
||||
'uptime': uptime,
|
||||
'hostname': hostname,
|
||||
'version': version,
|
||||
'error': None,
|
||||
'transport': 'ssh',
|
||||
'server': None,
|
||||
'auth_mode': 'ssh',
|
||||
'http_status': None,
|
||||
'backup_available': None,
|
||||
}
|
||||
except Exception as exc:
|
||||
return {
|
||||
"success": False,
|
||||
"tested_at": tested_at,
|
||||
"model": "Unknown",
|
||||
"uptime": "Unknown",
|
||||
"hostname": router.name,
|
||||
"version": None,
|
||||
"error": str(exc),
|
||||
'success': False,
|
||||
'tested_at': tested_at,
|
||||
'model': 'Unknown',
|
||||
'uptime': 'Unknown',
|
||||
'hostname': router.name,
|
||||
'version': None,
|
||||
'error': str(exc),
|
||||
'transport': 'ssh',
|
||||
'server': None,
|
||||
'auth_mode': 'ssh',
|
||||
'http_status': None,
|
||||
'backup_available': None,
|
||||
}
|
||||
|
||||
def probe_connection(self, router: Router, global_ssh_key: str | None = None, global_settings=None):
|
||||
if router.device_type == 'switchos':
|
||||
return swos_beta_service.probe_router(router, global_settings)
|
||||
return self._probe_routeros_connection(router, global_ssh_key)
|
||||
|
||||
def _store_connection_result(self, db: Session, router: Router, result: dict):
|
||||
router.last_connection_status = result["success"]
|
||||
router.last_connection_tested_at = result["tested_at"]
|
||||
router.last_connection_error = result.get("error")
|
||||
router.last_connection_hostname = result.get("hostname")
|
||||
router.last_connection_model = result.get("model")
|
||||
router.last_connection_version = result.get("version")
|
||||
router.last_connection_uptime = result.get("uptime")
|
||||
router.last_connection_status = result['success']
|
||||
router.last_connection_tested_at = result['tested_at']
|
||||
router.last_connection_error = result.get('error')
|
||||
router.last_connection_hostname = result.get('hostname')
|
||||
router.last_connection_model = result.get('model')
|
||||
router.last_connection_version = result.get('version')
|
||||
router.last_connection_uptime = result.get('uptime')
|
||||
router.last_connection_transport = result.get('transport')
|
||||
router.last_connection_server = result.get('server')
|
||||
router.last_connection_auth_mode = result.get('auth_mode')
|
||||
router.last_connection_http_status = result.get('http_status')
|
||||
router.last_connection_backup_available = result.get('backup_available')
|
||||
db.add(router)
|
||||
db.commit()
|
||||
db.refresh(router)
|
||||
return result
|
||||
|
||||
def test_connection(self, db: Session, router: Router, global_ssh_key: str | None = None):
|
||||
result = self.probe_connection(router, global_ssh_key)
|
||||
def test_connection(self, db: Session, router: Router, global_settings):
|
||||
result = self.probe_connection(router, global_settings.global_ssh_key, global_settings)
|
||||
return self._store_connection_result(db, router, result)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
@@ -41,7 +40,55 @@ class SwosBetaService:
|
||||
server=response.headers.get('server'),
|
||||
save_backup_visible='save backup' in html.lower(),
|
||||
backup_endpoint_ok=backup_ok,
|
||||
note='Moduł działa osobno i nie zapisuje kopii do głównego repozytorium.'
|
||||
note='SwitchOS jest obsługiwany bezpośrednio w liście urządzeń.'
|
||||
)
|
||||
|
||||
def probe_router(self, router, global_settings) -> dict:
|
||||
payload = self.credentials_from_router(router, global_settings)
|
||||
tested_at = datetime.utcnow()
|
||||
try:
|
||||
result = self.probe(payload)
|
||||
return {
|
||||
'success': result.success,
|
||||
'tested_at': tested_at,
|
||||
'model': 'SwitchOS',
|
||||
'uptime': f'HTTP {result.status_code}',
|
||||
'hostname': result.page_title or router.name,
|
||||
'version': None,
|
||||
'error': None,
|
||||
'transport': 'http',
|
||||
'server': result.server,
|
||||
'auth_mode': result.auth_mode,
|
||||
'http_status': str(result.status_code),
|
||||
'backup_available': result.backup_endpoint_ok,
|
||||
}
|
||||
except Exception as exc:
|
||||
return {
|
||||
'success': False,
|
||||
'tested_at': tested_at,
|
||||
'model': 'SwitchOS',
|
||||
'uptime': 'HTTP',
|
||||
'hostname': router.name,
|
||||
'version': None,
|
||||
'error': str(exc),
|
||||
'transport': 'http',
|
||||
'server': None,
|
||||
'auth_mode': None,
|
||||
'http_status': None,
|
||||
'backup_available': None,
|
||||
}
|
||||
|
||||
def credentials_from_router(self, router, global_settings) -> SwosBetaCredentials:
|
||||
username = (getattr(router, 'ssh_user', None) or '').strip() or (getattr(global_settings, 'default_switchos_username', None) or '').strip()
|
||||
password = (getattr(router, 'ssh_password', None) or '').strip() or (getattr(global_settings, 'default_switchos_password', None) or '').strip()
|
||||
if not username:
|
||||
raise ValueError('Brak użytkownika SwitchOS. Ustaw dane w urządzeniu albo w ustawieniach globalnych.')
|
||||
return SwosBetaCredentials(
|
||||
host=router.host,
|
||||
port=router.port or 80,
|
||||
username=username,
|
||||
password=password,
|
||||
label=router.name,
|
||||
)
|
||||
|
||||
def download_backup(self, payload: SwosBetaCredentials) -> DownloadedSwosBackup:
|
||||
@@ -62,6 +109,9 @@ class SwosBetaService:
|
||||
base_url=base_url,
|
||||
)
|
||||
|
||||
def download_backup_for_router(self, router, global_settings) -> DownloadedSwosBackup:
|
||||
return self.download_backup(self.credentials_from_router(router, global_settings))
|
||||
|
||||
def _request_with_fallback(self, method: str, url: str, payload: SwosBetaCredentials, allow_text_fallback: bool = True):
|
||||
attempts = []
|
||||
auth_variants = [
|
||||
@@ -89,8 +139,8 @@ class SwosBetaService:
|
||||
attempts.append(f'{label}:{exc.__class__.__name__}')
|
||||
|
||||
if last_response is not None:
|
||||
raise ValueError(f'Nie udało się połączyć ze SwOS ({", ".join(attempts)}).')
|
||||
raise ValueError('Nie udało się połączyć ze SwOS.')
|
||||
raise ValueError(f'Nie udało się połączyć ze SwitchOS ({", ".join(attempts)}).')
|
||||
raise ValueError('Nie udało się połączyć ze SwitchOS.')
|
||||
|
||||
def _build_base_url(self, host: str, port: int) -> str:
|
||||
raw = host.strip()
|
||||
@@ -118,7 +168,7 @@ class SwosBetaService:
|
||||
label = payload.label or payload.host
|
||||
safe = re.sub(r'[^A-Za-z0-9._-]+', '-', label).strip('-') or 'switchos'
|
||||
timestamp = datetime.now().strftime('%Y%m%d-%H%M%S')
|
||||
return f'{safe}-swos-{timestamp}.swb'
|
||||
return f'{safe}-switchos-{timestamp}.swb'
|
||||
|
||||
|
||||
swos_beta_service = SwosBetaService()
|
||||
|
||||
@@ -1,74 +1,151 @@
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.main import app
|
||||
from app.schemas.swos_beta import SwosBetaProbeResponse
|
||||
|
||||
|
||||
def _login(client: TestClient) -> str:
|
||||
def _login(client: TestClient) -> tuple[str, dict[str, str]]:
|
||||
response = client.post('/api/auth/login', data={'username': 'admin', 'password': 'admin'})
|
||||
return response.json()['access_token']
|
||||
token = response.json()['access_token']
|
||||
return token, {'Authorization': f'Bearer {token}'}
|
||||
|
||||
|
||||
def test_swos_probe_endpoint(monkeypatch, tmp_path):
|
||||
monkeypatch.setenv('DATABASE_URL', f'sqlite:///{tmp_path / "swos_probe.db"}')
|
||||
monkeypatch.setenv('DATA_DIR', str(tmp_path / 'data'))
|
||||
monkeypatch.setenv('SECRET_KEY', 'test-secret')
|
||||
monkeypatch.setenv('DEFAULT_ADMIN_USERNAME', 'admin')
|
||||
monkeypatch.setenv('DEFAULT_ADMIN_PASSWORD', 'admin')
|
||||
|
||||
from app.api.routes import swos_beta
|
||||
|
||||
monkeypatch.setattr(
|
||||
swos_beta.swos_beta_service,
|
||||
'probe',
|
||||
lambda payload: SwosBetaProbeResponse(
|
||||
success=True,
|
||||
base_url='http://192.168.88.1',
|
||||
status_code=200,
|
||||
auth_mode='digest',
|
||||
page_title='SwOS',
|
||||
content_type='text/html',
|
||||
server='MikroTik',
|
||||
save_backup_visible=True,
|
||||
backup_endpoint_ok=True,
|
||||
note='beta',
|
||||
),
|
||||
)
|
||||
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:
|
||||
token = _login(client)
|
||||
response = client.post(
|
||||
'/api/swos-beta/probe',
|
||||
json={'host': '192.168.88.1', 'port': 80, 'username': 'admin', 'password': ''},
|
||||
headers={'Authorization': f'Bearer {token}'},
|
||||
_, 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 response.status_code == 200
|
||||
assert response.json()['backup_endpoint_ok'] is True
|
||||
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_swos_download_endpoint(monkeypatch, tmp_path):
|
||||
monkeypatch.setenv('DATABASE_URL', f'sqlite:///{tmp_path / "swos_download.db"}')
|
||||
monkeypatch.setenv('DATA_DIR', str(tmp_path / 'data'))
|
||||
monkeypatch.setenv('SECRET_KEY', 'test-secret')
|
||||
monkeypatch.setenv('DEFAULT_ADMIN_USERNAME', 'admin')
|
||||
monkeypatch.setenv('DEFAULT_ADMIN_PASSWORD', 'admin')
|
||||
|
||||
from app.api.routes import swos_beta
|
||||
|
||||
class FakeBackup:
|
||||
filename = 'switch.swb'
|
||||
content = b'binary-data'
|
||||
content_type = 'application/octet-stream'
|
||||
|
||||
monkeypatch.setattr(swos_beta.swos_beta_service, 'download_backup', lambda payload: FakeBackup())
|
||||
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:
|
||||
token = _login(client)
|
||||
response = client.post(
|
||||
'/api/swos-beta/download',
|
||||
json={'host': '192.168.88.1', 'port': 80, 'username': 'admin', 'password': ''},
|
||||
headers={'Authorization': f'Bearer {token}'},
|
||||
_, 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': '2026-04-13T10:00:00',
|
||||
'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.content == b'binary-data'
|
||||
assert 'attachment; filename="switch.swb"' == response.headers['content-disposition']
|
||||
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')
|
||||
|
||||
@@ -56,10 +56,9 @@ export class AppComponent {
|
||||
{ label: 'nav.routers', link: '/routers', icon: 'pi pi-server', exact: false },
|
||||
{ label: 'nav.files', link: '/files', icon: 'pi pi-folder-open', exact: false },
|
||||
{ label: 'nav.diffConfigs', link: '/diff-configs', icon: 'pi pi-code', exact: false },
|
||||
{ label: 'nav.settings', link: '/settings', icon: 'pi pi-cog', exact: false },
|
||||
{ label: 'nav.logs', link: '/logs', icon: 'pi pi-history', exact: false },
|
||||
{ label: 'nav.switchosBeta', link: '/switchos-beta', icon: 'pi pi-sitemap', exact: false },
|
||||
{ label: 'nav.changePassword', link: '/change-password', icon: 'pi pi-lock', exact: false }
|
||||
{ label: 'nav.changePassword', link: '/change-password', icon: 'pi pi-lock', exact: false },
|
||||
{ label: 'nav.settings', link: '/settings', icon: 'pi pi-cog', exact: false }
|
||||
];
|
||||
|
||||
get currentPageTitle(): string {
|
||||
@@ -133,10 +132,6 @@ export class AppComponent {
|
||||
this.pageLabel = 'logs.title';
|
||||
return;
|
||||
}
|
||||
if (url.startsWith('/switchos-beta')) {
|
||||
this.pageLabel = 'switchosBeta.title';
|
||||
return;
|
||||
}
|
||||
if (url.startsWith('/change-password')) {
|
||||
this.pageLabel = 'auth.changePassword';
|
||||
return;
|
||||
|
||||
@@ -11,7 +11,6 @@ import { LogsPageComponent } from './features/logs/logs-page.component';
|
||||
import { RouterDetailPageComponent } from './features/routers/router-detail-page.component';
|
||||
import { RoutersPageComponent } from './features/routers/routers-page.component';
|
||||
import { SettingsPageComponent } from './features/settings/settings-page.component';
|
||||
import { SwosBetaPageComponent } from './features/swos-beta/swos-beta-page.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{ path: 'login', component: LoginPageComponent },
|
||||
@@ -24,6 +23,5 @@ export const routes: Routes = [
|
||||
{ path: 'diff-configs', canActivate: [authGuard], component: DiffConfigsPageComponent },
|
||||
{ path: 'settings', canActivate: [authGuard], component: SettingsPageComponent },
|
||||
{ path: 'logs', canActivate: [authGuard], component: LogsPageComponent },
|
||||
{ path: 'switchos-beta', canActivate: [authGuard], component: SwosBetaPageComponent },
|
||||
{ path: '**', redirectTo: '' }
|
||||
];
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
</td>
|
||||
<td>
|
||||
<div class="table-primary">{{ item.router_name || item.router_id }}</div>
|
||||
<small class="table-secondary">ID {{ item.router_id }}</small>
|
||||
<small class="table-secondary">{{ deviceLabel(item) }} · ID {{ item.router_id }}</small>
|
||||
</td>
|
||||
<td><p-tag [value]="item.backup_type === 'export' ? ('files.exportType' | translate) : ('files.binaryType' | translate)" [severity]="item.backup_type === 'export' ? 'success' : 'warning'"></p-tag></td>
|
||||
<td>
|
||||
@@ -113,7 +113,7 @@
|
||||
</td>
|
||||
<td>
|
||||
<div class="table-primary">{{ formatBytes(item.file_size) }}</div>
|
||||
<small class="table-secondary">{{ item.backup_type === 'export' ? '.rsc' : '.backup' }}</small>
|
||||
<small class="table-secondary">{{ fileExtension(item) }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<div class="table-actions table-actions--stack" *ngIf="item.backup_type === 'export'; else noCompare">
|
||||
@@ -130,7 +130,7 @@
|
||||
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" icon="pi pi-download" [label]="'common.download' | translate" (click)="download(item.id)"></button>
|
||||
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="secondary" icon="pi pi-envelope" [label]="'common.email' | translate" (click)="sendEmail(item.id)"></button>
|
||||
<button *ngIf="item.backup_type==='export'" pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="info" icon="pi pi-eye" [label]="'common.preview' | translate" (click)="viewExport(item)"></button>
|
||||
<button *ngIf="item.backup_type==='binary'" pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="help" icon="pi pi-upload" [label]="'common.restore' | translate" (click)="upload(item)"></button>
|
||||
<button *ngIf="item.backup_type==='binary' && item.device_type==='routeros'" pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="help" icon="pi pi-upload" [label]="'common.restore' | translate" (click)="upload(item)"></button>
|
||||
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide" severity="danger" icon="pi pi-trash" [label]="'common.delete' | translate" (click)="deleteOne(item.id)"></button>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -16,10 +16,13 @@ import { PageHeaderComponent } from '../../shared/ui/page-header.component';
|
||||
import { SectionCardComponent } from '../../shared/ui/section-card.component';
|
||||
import { StatCardComponent } from '../../shared/ui/stat-card.component';
|
||||
|
||||
type DeviceType = 'routeros' | 'switchos';
|
||||
|
||||
interface BackupFile {
|
||||
id: number;
|
||||
router_id: number;
|
||||
router_name?: string;
|
||||
device_type: DeviceType;
|
||||
file_name: string;
|
||||
backup_type: 'export' | 'binary';
|
||||
created_at: string;
|
||||
@@ -233,6 +236,9 @@ export class FilesPageComponent implements OnInit {
|
||||
}
|
||||
|
||||
upload(item: BackupFile) {
|
||||
if (item.device_type !== 'routeros') {
|
||||
return;
|
||||
}
|
||||
this.api.http.post(`${this.api.baseUrl}/backups/router/${item.router_id}/upload/${item.id}`, {}).subscribe(() => {
|
||||
this.ui.success('toast.binaryUploaded');
|
||||
});
|
||||
@@ -410,6 +416,15 @@ export class FilesPageComponent implements OnInit {
|
||||
return `${value.slice(0, 8)}…${value.slice(-6)}`;
|
||||
}
|
||||
|
||||
deviceLabel(item: BackupFile): string {
|
||||
return this.ui.instant(item.device_type === 'switchos' ? 'routers.switchos' : 'routers.routeros');
|
||||
}
|
||||
|
||||
fileExtension(item: BackupFile): string {
|
||||
const parts = item.file_name.split('.');
|
||||
return parts.length > 1 ? `.${parts[parts.length - 1]}` : '—';
|
||||
}
|
||||
|
||||
private setComparePair(firstId: number, secondId: number) {
|
||||
const [left, right] = this.sortPairByDate(firstId, secondId);
|
||||
this.compareLeftId = left;
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<app-page-header
|
||||
[eyebrow]="'routers.profileEyebrow' | translate"
|
||||
[title]="routerItem?.name || ('routers.detailTitle' | translate)"
|
||||
[subtitle]="routerItem ? routerItem.host + ':' + routerItem.port + ' · ' + routerItem.ssh_user : ('routers.detailSubtitle' | translate)"
|
||||
[subtitle]="subtitle"
|
||||
>
|
||||
<div header-actions class="header-actions-row">
|
||||
<button pButton type="button" icon="pi pi-upload" [label]="'routers.exportOne' | translate" [loading]="exporting" (click)="runExport()"></button>
|
||||
<button pButton type="button" severity="secondary" icon="pi pi-database" [label]="'routers.binaryOne' | translate" [loading]="runningBinary" (click)="runBinary()"></button>
|
||||
<button *ngIf="!isSwitchos" pButton type="button" icon="pi pi-upload" [label]="'routers.exportOne' | translate" [loading]="exporting" (click)="runExport()"></button>
|
||||
<button pButton type="button" severity="secondary" icon="pi pi-database" [label]="(isSwitchos ? 'routers.downloadSwitchBackup' : 'routers.binaryOne') | translate" [loading]="runningBinary" (click)="runBinary()"></button>
|
||||
<button pButton type="button" severity="info" icon="pi pi-wifi" [label]="'routers.testConnection' | translate" [loading]="testing" (click)="testConnection()"></button>
|
||||
<button pButton type="button" severity="danger" icon="pi pi-trash" [label]="'routers.deleteRouter' | translate" [loading]="deletingRouter" (click)="deleteRouter()"></button>
|
||||
</div>
|
||||
</app-page-header>
|
||||
|
||||
<div class="stats-grid compact-grid">
|
||||
<app-stat-card [label]="'routers.exportsLabel' | translate" [value]="exportBackups.length" [hint]="'routers.exportsLabelHint' | translate" [tag]="'files.exportType' | translate" severity="success" icon="pi pi-file-export" iconClass="icon-emerald"></app-stat-card>
|
||||
<app-stat-card [label]="'routers.deviceType' | translate" [value]="deviceTypeLabel" [hint]="'routers.listSubtitle' | translate" [tag]="'routers.fleetTag' | translate" severity="info" icon="pi pi-sitemap" iconClass="icon-blue"></app-stat-card>
|
||||
<app-stat-card [label]="'routers.binaryLabel' | translate" [value]="binaryBackups.length" [hint]="'routers.binaryLabelHint' | translate" [tag]="'files.binaryType' | translate" severity="warning" icon="pi pi-database" iconClass="icon-amber"></app-stat-card>
|
||||
<app-stat-card [label]="'routers.connectionLabel' | translate" [value]="connectionStateLabel" [hint]="'routers.connectionLabelHint' | translate" [tag]="'routers.probeTag' | translate" severity="info" icon="pi pi-bolt" iconClass="icon-blue"></app-stat-card>
|
||||
<app-stat-card [label]="'routers.sshUser' | translate" [value]="routerItem?.ssh_user || '-'" [hint]="'routers.sshUserHint' | translate" [tag]="'routers.accessTag' | translate" severity="secondary" icon="pi pi-user" iconClass="icon-violet"></app-stat-card>
|
||||
<app-stat-card [label]="'routers.sshUser' | translate" [value]="routerItem?.effective_username || '-'" [hint]="'routers.sshUserHint' | translate" [tag]="'routers.accessTag' | translate" severity="secondary" icon="pi pi-user" iconClass="icon-violet"></app-stat-card>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-grid router-detail-grid router-detail-grid--inspection">
|
||||
@@ -28,6 +28,10 @@
|
||||
<div class="metric-tile"><span>{{ 'routers.model' | translate }}</span><strong>{{ connection.model }}</strong></div>
|
||||
<div class="metric-tile"><span>{{ 'routers.version' | translate }}</span><strong>{{ connection.version || 'n/a' }}</strong></div>
|
||||
<div class="metric-tile"><span>{{ 'routers.uptime' | translate }}</span><strong>{{ connection.uptime }}</strong></div>
|
||||
<div class="metric-tile" *ngIf="isSwitchos"><span>{{ 'routers.httpStatus' | translate }}</span><strong>{{ connection.http_status || '—' }}</strong></div>
|
||||
<div class="metric-tile" *ngIf="isSwitchos"><span>{{ 'routers.serverHeader' | translate }}</span><strong>{{ connection.server || '—' }}</strong></div>
|
||||
<div class="metric-tile" *ngIf="isSwitchos"><span>{{ 'routers.authMode' | translate }}</span><strong>{{ connection.auth_mode || '—' }}</strong></div>
|
||||
<div class="metric-tile" *ngIf="isSwitchos"><span>{{ 'routers.backupEndpoint' | translate }}</span><strong>{{ connection.backup_available ? ('routers.backupAvailable' | translate) : ('routers.backupUnavailable' | translate) }}</strong></div>
|
||||
</div>
|
||||
<div class="router-status-error" *ngIf="!connection.success && connection.error">
|
||||
<strong>{{ 'routers.lastError' | translate }}</strong>
|
||||
@@ -42,7 +46,7 @@
|
||||
</ng-template>
|
||||
</app-section-card>
|
||||
|
||||
<div class="router-detail-inspection-stack">
|
||||
<div class="router-detail-inspection-stack" *ngIf="!isSwitchos">
|
||||
<app-section-card [title]="'routers.previewTitle' | translate" [subtitle]="'routers.previewSubtitle' | translate">
|
||||
<div class="router-modal-summary" *ngIf="hasPreview; else noPreview">
|
||||
<div>
|
||||
@@ -81,7 +85,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-grid router-detail-grid router-detail-grid--stack">
|
||||
<div class="dashboard-grid router-detail-grid router-detail-grid--stack" *ngIf="!isSwitchos">
|
||||
<app-section-card [title]="'routers.exportsTableTitle' | translate" [subtitle]="'routers.exportsTableSubtitle' | translate">
|
||||
<p-table [value]="exportBackups" responsiveLayout="scroll" styleClass="app-table">
|
||||
<ng-template pTemplate="header">
|
||||
@@ -107,7 +111,9 @@
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</app-section-card>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-grid router-detail-grid router-detail-grid--stack">
|
||||
<app-section-card [title]="'routers.binaryTableTitle' | translate" [subtitle]="'routers.binaryTableSubtitle' | translate">
|
||||
<p-table [value]="binaryBackups" responsiveLayout="scroll" styleClass="app-table">
|
||||
<ng-template pTemplate="header">
|
||||
@@ -123,7 +129,7 @@
|
||||
<td>
|
||||
<div class="table-actions table-actions--labels table-actions--tight">
|
||||
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" icon="pi pi-download" [label]="'common.download' | translate" (click)="download(item.id)"></button>
|
||||
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" severity="help" icon="pi pi-upload" [label]="'common.restore' | translate" (click)="upload(item.id)"></button>
|
||||
<button *ngIf="!isSwitchos" pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" severity="help" icon="pi pi-upload" [label]="'common.restore' | translate" (click)="upload(item.id)"></button>
|
||||
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" severity="secondary" icon="pi pi-envelope" [label]="'common.email' | translate" (click)="sendEmail(item.id)"></button>
|
||||
<button pButton type="button" size="small" styleClass="table-action-btn table-action-btn--wide table-action-btn--compact" severity="danger" icon="pi pi-trash" [label]="'common.delete' | translate" (click)="remove(item.id)"></button>
|
||||
</div>
|
||||
|
||||
@@ -14,11 +14,37 @@ import { PageHeaderComponent } from '../../shared/ui/page-header.component';
|
||||
import { SectionCardComponent } from '../../shared/ui/section-card.component';
|
||||
import { StatCardComponent } from '../../shared/ui/stat-card.component';
|
||||
|
||||
type DeviceType = 'routeros' | 'switchos';
|
||||
|
||||
interface DeviceItem {
|
||||
id: number;
|
||||
name: string;
|
||||
host: string;
|
||||
port: number;
|
||||
device_type: DeviceType;
|
||||
effective_username?: string | null;
|
||||
supports_export: boolean;
|
||||
supports_restore_upload: boolean;
|
||||
last_connection_status?: boolean | null;
|
||||
last_connection_tested_at?: string | null;
|
||||
last_connection_error?: string | null;
|
||||
last_connection_hostname?: string | null;
|
||||
last_connection_model?: string | null;
|
||||
last_connection_version?: string | null;
|
||||
last_connection_uptime?: string | null;
|
||||
last_connection_transport?: string | null;
|
||||
last_connection_server?: string | null;
|
||||
last_connection_auth_mode?: string | null;
|
||||
last_connection_http_status?: string | null;
|
||||
last_connection_backup_available?: boolean | null;
|
||||
}
|
||||
|
||||
interface BackupItem {
|
||||
id: number;
|
||||
file_name: string;
|
||||
backup_type: 'export' | 'binary';
|
||||
created_at: string;
|
||||
device_type: DeviceType;
|
||||
}
|
||||
|
||||
interface ConnectionSnapshot {
|
||||
@@ -29,6 +55,11 @@ interface ConnectionSnapshot {
|
||||
version?: string | null;
|
||||
uptime: string;
|
||||
error?: string | null;
|
||||
transport?: string | null;
|
||||
server?: string | null;
|
||||
auth_mode?: string | null;
|
||||
http_status?: string | null;
|
||||
backup_available?: boolean | null;
|
||||
}
|
||||
|
||||
interface BackupDiffStats {
|
||||
@@ -59,7 +90,7 @@ export class RouterDetailPageComponent implements OnInit {
|
||||
private readonly ui = inject(UiService);
|
||||
|
||||
routerId!: number;
|
||||
routerItem: any;
|
||||
routerItem: DeviceItem | null = null;
|
||||
backups: BackupItem[] = [];
|
||||
connection: ConnectionSnapshot | null = null;
|
||||
exportContent = '';
|
||||
@@ -73,6 +104,10 @@ export class RouterDetailPageComponent implements OnInit {
|
||||
testing = false;
|
||||
deletingRouter = false;
|
||||
|
||||
get isSwitchos(): boolean {
|
||||
return this.routerItem?.device_type === 'switchos';
|
||||
}
|
||||
|
||||
get exportBackups(): BackupItem[] {
|
||||
return this.backups.filter((item) => item.backup_type === 'export');
|
||||
}
|
||||
@@ -96,13 +131,25 @@ export class RouterDetailPageComponent implements OnInit {
|
||||
return !!this.diffText;
|
||||
}
|
||||
|
||||
get subtitle(): string {
|
||||
if (!this.routerItem) {
|
||||
return this.ui.instant('routers.detailSubtitle');
|
||||
}
|
||||
const suffix = this.routerItem.effective_username ? ` · ${this.routerItem.effective_username}` : '';
|
||||
return `${this.routerItem.host}:${this.routerItem.port}${suffix}`;
|
||||
}
|
||||
|
||||
get deviceTypeLabel(): string {
|
||||
return this.ui.instant(this.isSwitchos ? 'routers.switchos' : 'routers.routeros');
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.routerId = Number(this.route.snapshot.paramMap.get('id'));
|
||||
this.load();
|
||||
}
|
||||
|
||||
load() {
|
||||
this.api.http.get(`${this.api.baseUrl}/routers/${this.routerId}`).subscribe((routerItem: any) => {
|
||||
this.api.http.get<DeviceItem>(`${this.api.baseUrl}/routers/${this.routerId}`).subscribe((routerItem) => {
|
||||
this.routerItem = routerItem;
|
||||
this.connection = this.mapStoredConnection(routerItem);
|
||||
});
|
||||
@@ -110,7 +157,7 @@ export class RouterDetailPageComponent implements OnInit {
|
||||
}
|
||||
|
||||
runExport() {
|
||||
if (this.exporting) {
|
||||
if (this.exporting || this.isSwitchos) {
|
||||
return;
|
||||
}
|
||||
this.exporting = true;
|
||||
@@ -187,6 +234,9 @@ export class RouterDetailPageComponent implements OnInit {
|
||||
}
|
||||
|
||||
upload(id: number) {
|
||||
if (this.isSwitchos) {
|
||||
return;
|
||||
}
|
||||
this.api.http.post(`${this.api.baseUrl}/backups/router/${this.routerId}/upload/${id}`, {}).subscribe(() => {
|
||||
this.ui.success('toast.binaryUploaded');
|
||||
});
|
||||
@@ -217,7 +267,7 @@ export class RouterDetailPageComponent implements OnInit {
|
||||
|
||||
viewExport(id: number) {
|
||||
const backup = this.exportBackups.find((item) => item.id === id);
|
||||
this.api.http.get<any>(`${this.api.baseUrl}/backups/${id}/view`).subscribe((r) => {
|
||||
this.api.http.get<{ content: string }>(`${this.api.baseUrl}/backups/${id}/view`).subscribe((r) => {
|
||||
this.exportContent = r.content;
|
||||
this.previewTitle = backup?.file_name || this.ui.instant('routers.previewTitle');
|
||||
this.ui.clear();
|
||||
@@ -241,7 +291,7 @@ export class RouterDetailPageComponent implements OnInit {
|
||||
this.diffVisible = true;
|
||||
}
|
||||
|
||||
private mapStoredConnection(routerItem: any): ConnectionSnapshot | null {
|
||||
private mapStoredConnection(routerItem: DeviceItem): ConnectionSnapshot | null {
|
||||
if (!routerItem?.last_connection_tested_at) {
|
||||
return null;
|
||||
}
|
||||
@@ -252,7 +302,12 @@ export class RouterDetailPageComponent implements OnInit {
|
||||
model: routerItem.last_connection_model || 'Unknown',
|
||||
version: routerItem.last_connection_version,
|
||||
uptime: routerItem.last_connection_uptime || 'Unknown',
|
||||
error: routerItem.last_connection_error || null
|
||||
error: routerItem.last_connection_error || null,
|
||||
transport: routerItem.last_connection_transport || null,
|
||||
server: routerItem.last_connection_server || null,
|
||||
auth_mode: routerItem.last_connection_auth_mode || null,
|
||||
http_status: routerItem.last_connection_http_status || null,
|
||||
backup_available: routerItem.last_connection_backup_available ?? null
|
||||
};
|
||||
}
|
||||
|
||||
@@ -269,6 +324,11 @@ export class RouterDetailPageComponent implements OnInit {
|
||||
last_connection_version: result.version,
|
||||
last_connection_uptime: result.uptime,
|
||||
last_connection_error: result.error,
|
||||
last_connection_transport: result.transport,
|
||||
last_connection_server: result.server,
|
||||
last_connection_auth_mode: result.auth_mode,
|
||||
last_connection_http_status: result.http_status,
|
||||
last_connection_backup_available: result.backup_available
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -11,13 +11,13 @@
|
||||
</div>
|
||||
<div class="inline-summary__divider"></div>
|
||||
<div class="inline-summary__item">
|
||||
<strong>{{ keyCount }}</strong>
|
||||
<span>{{ 'routers.summaryKeyAccess' | translate }}</span>
|
||||
<strong>{{ routerOsCount }}</strong>
|
||||
<span>{{ 'routers.routeros' | translate }}</span>
|
||||
</div>
|
||||
<div class="inline-summary__divider"></div>
|
||||
<div class="inline-summary__item">
|
||||
<strong>{{ passwordCount }}</strong>
|
||||
<span>{{ 'routers.summaryPasswordAccess' | translate }}</span>
|
||||
<strong>{{ switchOsCount }}</strong>
|
||||
<span>{{ 'routers.switchos' | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,16 +30,16 @@
|
||||
<tr>
|
||||
<td>
|
||||
<div class="table-primary">{{ routerItem.name }}</div>
|
||||
<small class="table-secondary">{{ 'routers.routerOsTarget' | translate }}</small>
|
||||
<small class="table-secondary">{{ deviceTypeLabel(routerItem) }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<div class="table-primary">{{ routerItem.host }}:{{ routerItem.port }}</div>
|
||||
<small class="table-secondary">{{ routerItem.ssh_user }}</small>
|
||||
<small class="table-secondary">{{ accessUser(routerItem) }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<div class="inline-tags">
|
||||
<p-tag [value]="routerItem.ssh_password ? ('routers.passwordMode' | translate) : ('routers.noPassword' | translate)" [severity]="routerItem.ssh_password ? 'warning' : 'secondary'"></p-tag>
|
||||
<p-tag [value]="hasEffectiveSshKey(routerItem) ? ((usesGlobalSshKey(routerItem) ? 'routers.globalKeyMode' : 'routers.keyMode') | translate) : ('routers.noKey' | translate)" [severity]="hasEffectiveSshKey(routerItem) ? 'success' : 'secondary'"></p-tag>
|
||||
<p-tag [value]="primaryAccessTag(routerItem).value" [severity]="primaryAccessTag(routerItem).severity"></p-tag>
|
||||
<p-tag [value]="secondaryAccessTag(routerItem).value" [severity]="secondaryAccessTag(routerItem).severity"></p-tag>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
@@ -54,33 +54,101 @@
|
||||
</p-table>
|
||||
</app-section-card>
|
||||
|
||||
<p-dialog [(visible)]="visible" [modal]="true" [header]="dialogTitle" [style]="{ width: '640px' }" styleClass="router-dialog">
|
||||
<form [formGroup]="form" (ngSubmit)="save()" class="form-grid-2">
|
||||
<span class="form-field">
|
||||
<label>{{ 'routers.name' | translate }}</label>
|
||||
<input pInputText formControlName="name" placeholder="core-router-waw" />
|
||||
</span>
|
||||
<span class="form-field">
|
||||
<label>{{ 'routers.host' | translate }}</label>
|
||||
<input pInputText formControlName="host" placeholder="10.0.0.1" />
|
||||
</span>
|
||||
<span class="form-field">
|
||||
<label>{{ 'routers.port' | translate }}</label>
|
||||
<input pInputText type="number" formControlName="port" placeholder="22" />
|
||||
</span>
|
||||
<span class="form-field">
|
||||
<label>{{ 'routers.sshUser' | translate }}</label>
|
||||
<input pInputText formControlName="ssh_user" placeholder="admin" />
|
||||
</span>
|
||||
<span class="form-field form-field--full">
|
||||
<label>{{ 'routers.sshPassword' | translate }}</label>
|
||||
<input pInputText formControlName="ssh_password" [placeholder]="'routers.optionalPassword' | translate" />
|
||||
</span>
|
||||
<span class="form-field form-field--full">
|
||||
<label>{{ 'routers.sshPrivateKey' | translate }}</label>
|
||||
<textarea pInputTextarea formControlName="ssh_key" rows="7" [placeholder]="'routers.optionalPrivateKey' | translate"></textarea>
|
||||
</span>
|
||||
<div class="dialog-actions">
|
||||
<p-dialog [(visible)]="visible" [modal]="true" [draggable]="false" [resizable]="false" [style]="{ width: 'min(760px, 96vw)' }" styleClass="router-dialog">
|
||||
<ng-template pTemplate="header">
|
||||
<div class="router-dialog-header">
|
||||
<div class="router-dialog-header__icon">
|
||||
<i class="pi" [ngClass]="selectedDeviceType === 'switchos' ? 'pi-sitemap' : 'pi-server'"></i>
|
||||
</div>
|
||||
<div class="router-dialog-header__text">
|
||||
<div class="router-dialog-header__eyebrow">
|
||||
{{ 'routers.deviceType' | translate }} · {{ selectedDeviceType === 'switchos' ? ('routers.switchos' | translate) : ('routers.routeros' | translate) }}
|
||||
</div>
|
||||
<div class="router-dialog-header__title">{{ dialogTitle }}</div>
|
||||
<small>
|
||||
{{
|
||||
selectedDeviceType === 'switchos'
|
||||
? ('routers.switchDialogSubtitle' | translate)
|
||||
: ('routers.routerDialogSubtitle' | translate)
|
||||
}}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<form [formGroup]="form" (ngSubmit)="save()" class="router-dialog-form">
|
||||
<section class="router-dialog-panel">
|
||||
<div class="router-dialog-panel__header">
|
||||
<div>
|
||||
<strong>{{ 'routers.connectionSectionTitle' | translate }}</strong>
|
||||
<p>{{ 'routers.connectionSectionHint' | translate }}</p>
|
||||
</div>
|
||||
<span class="router-dialog-pill">
|
||||
<i class="pi" [ngClass]="selectedDeviceType === 'switchos' ? 'pi-globe' : 'pi-shield'"></i>
|
||||
{{ selectedDeviceType === 'switchos' ? 'HTTP' : 'SSH' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-grid-2 router-dialog-grid">
|
||||
<span class="form-field">
|
||||
<label>{{ 'routers.name' | translate }}</label>
|
||||
<input pInputText formControlName="name" placeholder="core-router-waw" />
|
||||
</span>
|
||||
<span class="form-field">
|
||||
<label>{{ 'routers.deviceType' | translate }}</label>
|
||||
<p-dropdown [options]="deviceTypeOptions" formControlName="device_type" optionLabel="label" optionValue="value"></p-dropdown>
|
||||
</span>
|
||||
<span class="form-field">
|
||||
<label>{{ 'routers.host' | translate }}</label>
|
||||
<input pInputText formControlName="host" placeholder="10.0.0.1" />
|
||||
</span>
|
||||
<span class="form-field">
|
||||
<label>{{ 'routers.port' | translate }}</label>
|
||||
<input pInputText type="number" formControlName="port" [placeholder]="selectedDeviceType === 'switchos' ? '80' : '22'" />
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="router-dialog-panel">
|
||||
<div class="router-dialog-panel__header">
|
||||
<div>
|
||||
<strong>{{ 'routers.credentialsSectionTitle' | translate }}</strong>
|
||||
<p>
|
||||
{{
|
||||
selectedDeviceType === 'switchos'
|
||||
? ('routers.switchDialogSubtitle' | translate)
|
||||
: ('routers.routerDialogSubtitle' | translate)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<span class="router-dialog-pill">
|
||||
<i class="pi pi-key"></i>
|
||||
{{ selectedDeviceType === 'switchos' ? ('routers.defaultCredentials' | translate) : 'SSH' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-grid-2 router-dialog-grid">
|
||||
<span class="form-field">
|
||||
<label>{{ 'routers.sshUser' | translate }}</label>
|
||||
<input pInputText formControlName="ssh_user" [placeholder]="selectedDeviceType === 'switchos' ? ('routers.switchUserPlaceholder' | translate) : 'admin'" />
|
||||
</span>
|
||||
<span class="form-field">
|
||||
<label>{{ 'routers.sshPassword' | translate }}</label>
|
||||
<input pInputText type="password" formControlName="ssh_password" [placeholder]="selectedDeviceType === 'switchos' ? ('routers.switchPasswordPlaceholder' | translate) : ('routers.optionalPassword' | translate)" />
|
||||
</span>
|
||||
<span class="form-field form-field--full" *ngIf="selectedDeviceType === 'routeros'">
|
||||
<label>{{ 'routers.sshPrivateKey' | translate }}</label>
|
||||
<textarea pInputTextarea formControlName="ssh_key" rows="8" [placeholder]="'routers.optionalPrivateKey' | translate"></textarea>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="router-dialog-note" *ngIf="selectedDeviceType === 'switchos'">
|
||||
<i class="pi pi-info-circle"></i>
|
||||
<span>{{ 'routers.switchDefaultsHint' | translate }}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="dialog-actions router-dialog-actions">
|
||||
<button pButton type="button" severity="secondary" [label]="'common.cancel' | translate" (click)="visible=false"></button>
|
||||
<button pButton type="submit" [disabled]="form.invalid || saving" [loading]="saving" [label]="'routers.saveRouter' | translate"></button>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Router } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ButtonModule } from 'primeng/button';
|
||||
import { DialogModule } from 'primeng/dialog';
|
||||
import { DropdownModule } from 'primeng/dropdown';
|
||||
import { InputTextareaModule } from 'primeng/inputtextarea';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { TableModule } from 'primeng/table';
|
||||
@@ -15,21 +16,40 @@ import { UiService } from '../../core/services/ui.service';
|
||||
import { PageHeaderComponent } from '../../shared/ui/page-header.component';
|
||||
import { SectionCardComponent } from '../../shared/ui/section-card.component';
|
||||
|
||||
type DeviceType = 'routeros' | 'switchos';
|
||||
|
||||
interface RouterItem {
|
||||
id: number;
|
||||
name: string;
|
||||
host: string;
|
||||
port: number;
|
||||
ssh_user: string;
|
||||
ssh_password?: string;
|
||||
ssh_key?: string;
|
||||
device_type: DeviceType;
|
||||
ssh_user?: string | null;
|
||||
ssh_password?: string | null;
|
||||
ssh_key?: string | null;
|
||||
effective_username?: string | null;
|
||||
uses_global_ssh_key?: boolean;
|
||||
has_effective_ssh_key?: boolean;
|
||||
uses_global_switchos_credentials?: boolean;
|
||||
has_effective_password?: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [CommonModule, ReactiveFormsModule, TranslateModule, ButtonModule, DialogModule, InputTextModule, InputTextareaModule, TableModule, TagModule, PageHeaderComponent, SectionCardComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
TranslateModule,
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
DropdownModule,
|
||||
InputTextModule,
|
||||
InputTextareaModule,
|
||||
TableModule,
|
||||
TagModule,
|
||||
PageHeaderComponent,
|
||||
SectionCardComponent
|
||||
],
|
||||
templateUrl: './routers-page.component.html'
|
||||
})
|
||||
export class RoutersPageComponent implements OnInit {
|
||||
@@ -42,11 +62,16 @@ export class RoutersPageComponent implements OnInit {
|
||||
editingId: number | null = null;
|
||||
saving = false;
|
||||
routers: RouterItem[] = [];
|
||||
readonly deviceTypeOptions = [
|
||||
{ label: 'RouterOS', value: 'routeros' },
|
||||
{ label: 'SwitchOS', value: 'switchos' }
|
||||
];
|
||||
readonly form = this.fb.nonNullable.group({
|
||||
name: ['', Validators.required],
|
||||
device_type: ['routeros' as DeviceType, Validators.required],
|
||||
host: ['', Validators.required],
|
||||
port: [22, Validators.required],
|
||||
ssh_user: ['admin', Validators.required],
|
||||
ssh_user: ['admin'],
|
||||
ssh_password: '',
|
||||
ssh_key: ''
|
||||
});
|
||||
@@ -55,24 +80,22 @@ export class RoutersPageComponent implements OnInit {
|
||||
return this.ui.instant(this.editingId ? 'routers.editDialogTitle' : 'routers.createDialogTitle');
|
||||
}
|
||||
|
||||
get passwordCount(): number {
|
||||
return this.routers.filter((item) => !!item.ssh_password).length;
|
||||
get selectedDeviceType(): DeviceType {
|
||||
return this.form.controls.device_type.value;
|
||||
}
|
||||
|
||||
get keyCount(): number {
|
||||
return this.routers.filter((item) => this.hasEffectiveSshKey(item)).length;
|
||||
get routerOsCount(): number {
|
||||
return this.routers.filter((item) => item.device_type === 'routeros').length;
|
||||
}
|
||||
|
||||
hasEffectiveSshKey(item: RouterItem): boolean {
|
||||
return !!item.has_effective_ssh_key;
|
||||
get switchOsCount(): number {
|
||||
return this.routers.filter((item) => item.device_type === 'switchos').length;
|
||||
}
|
||||
|
||||
usesGlobalSshKey(item: RouterItem): boolean {
|
||||
return !!item.uses_global_ssh_key;
|
||||
}
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
this.form.controls.device_type.valueChanges.subscribe((deviceType) => {
|
||||
this.applyDeviceDefaults((deviceType || 'routeros') as DeviceType);
|
||||
});
|
||||
this.load();
|
||||
}
|
||||
|
||||
@@ -82,7 +105,7 @@ export class RoutersPageComponent implements OnInit {
|
||||
|
||||
openCreate() {
|
||||
this.editingId = null;
|
||||
this.form.reset({ name: '', host: '', port: 22, ssh_user: 'admin', ssh_password: '', ssh_key: '' });
|
||||
this.form.reset({ name: '', device_type: 'routeros', host: '', port: 22, ssh_user: 'admin', ssh_password: '', ssh_key: '' });
|
||||
this.visible = true;
|
||||
}
|
||||
|
||||
@@ -90,9 +113,10 @@ export class RoutersPageComponent implements OnInit {
|
||||
this.editingId = item.id;
|
||||
this.form.reset({
|
||||
name: item.name,
|
||||
device_type: item.device_type,
|
||||
host: item.host,
|
||||
port: item.port,
|
||||
ssh_user: item.ssh_user,
|
||||
ssh_user: item.ssh_user ?? '',
|
||||
ssh_password: item.ssh_password ?? '',
|
||||
ssh_key: item.ssh_key ?? ''
|
||||
});
|
||||
@@ -104,9 +128,13 @@ export class RoutersPageComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
this.saving = true;
|
||||
const payload = this.form.getRawValue();
|
||||
if (payload.device_type === 'switchos') {
|
||||
payload.ssh_key = '';
|
||||
}
|
||||
const request$ = this.editingId
|
||||
? this.api.http.put(`${this.api.baseUrl}/routers/${this.editingId}`, this.form.getRawValue())
|
||||
: this.api.http.post(`${this.api.baseUrl}/routers`, this.form.getRawValue());
|
||||
? this.api.http.put(`${this.api.baseUrl}/routers/${this.editingId}`, payload)
|
||||
: this.api.http.post(`${this.api.baseUrl}/routers`, payload);
|
||||
|
||||
request$.subscribe({
|
||||
next: () => {
|
||||
@@ -134,8 +162,56 @@ export class RoutersPageComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
open(id: number) {
|
||||
this.router.navigate(['/routers', id]);
|
||||
}
|
||||
|
||||
deviceTypeLabel(item: RouterItem): string {
|
||||
return this.ui.instant(item.device_type === 'switchos' ? 'routers.switchos' : 'routers.routeros');
|
||||
}
|
||||
|
||||
accessUser(item: RouterItem): string {
|
||||
return item.effective_username || item.ssh_user || '—';
|
||||
}
|
||||
|
||||
primaryAccessTag(item: RouterItem): { value: string; severity: 'success' | 'warning' | 'secondary' | 'info' } {
|
||||
if (item.device_type === 'switchos') {
|
||||
if (item.uses_global_switchos_credentials) {
|
||||
return { value: this.ui.instant('routers.defaultCredentials'), severity: 'info' };
|
||||
}
|
||||
if (item.has_effective_password) {
|
||||
return { value: this.ui.instant('routers.localCredentials'), severity: 'success' };
|
||||
}
|
||||
return { value: this.ui.instant('routers.noCredentials'), severity: 'secondary' };
|
||||
}
|
||||
|
||||
return {
|
||||
value: item.ssh_password ? this.ui.instant('routers.passwordMode') : this.ui.instant('routers.noPassword'),
|
||||
severity: item.ssh_password ? 'warning' : 'secondary'
|
||||
};
|
||||
}
|
||||
|
||||
secondaryAccessTag(item: RouterItem): { value: string; severity: 'success' | 'warning' | 'secondary' | 'info' } {
|
||||
if (item.device_type === 'switchos') {
|
||||
return {
|
||||
value: item.has_effective_password ? this.ui.instant('routers.passwordMode') : this.ui.instant('routers.noPassword'),
|
||||
severity: item.has_effective_password ? 'warning' : 'secondary'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
value: item.has_effective_ssh_key
|
||||
? this.ui.instant(item.uses_global_ssh_key ? 'routers.globalKeyMode' : 'routers.keyMode')
|
||||
: this.ui.instant('routers.noKey'),
|
||||
severity: item.has_effective_ssh_key ? 'success' : 'secondary'
|
||||
};
|
||||
}
|
||||
|
||||
private applyDeviceDefaults(deviceType: DeviceType) {
|
||||
if (deviceType === 'switchos') {
|
||||
this.form.patchValue({ port: 80, ssh_key: '', ssh_user: this.form.controls.ssh_user.value || '' }, { emitEvent: false });
|
||||
return;
|
||||
}
|
||||
this.form.patchValue({ port: 22, ssh_user: this.form.controls.ssh_user.value || 'admin' }, { emitEvent: false });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,7 +253,7 @@
|
||||
</div>
|
||||
|
||||
<div class="settings-page-side">
|
||||
<details class="settings-collapse settings-collapse--sticky">
|
||||
<details class="settings-collapse settings-collapse--sticky" open>
|
||||
<summary>
|
||||
<span>{{ 'settings.sshDefaultsTitle' | translate }}</span>
|
||||
<small>{{ 'settings.sshDefaultsSubtitle' | translate }}</small>
|
||||
@@ -297,6 +297,25 @@
|
||||
|
||||
<small class="settings-ssh-note" *ngIf="clearStoredSshKey">{{ 'settings.sshKeyClearNotice' | translate }}</small>
|
||||
</div>
|
||||
|
||||
<div class="settings-ssh-panel">
|
||||
<div class="settings-ssh-panel__header">
|
||||
<div>
|
||||
<strong>{{ 'settings.switchosDefaultsTitle' | translate }}</strong>
|
||||
<p>{{ 'settings.switchosDefaultsHint' | translate }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-grid-2">
|
||||
<span class="form-field">
|
||||
<label>{{ 'settings.defaultSwitchosUsername' | translate }}</label>
|
||||
<input pInputText formControlName="default_switchos_username" placeholder="admin" />
|
||||
</span>
|
||||
<span class="form-field">
|
||||
<label>{{ 'settings.defaultSwitchosPassword' | translate }}</label>
|
||||
<input pInputText formControlName="default_switchos_password" placeholder="••••••••" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -55,6 +55,9 @@ interface SettingsResponse {
|
||||
connection_test_interval_minutes: number;
|
||||
global_ssh_key: string | null;
|
||||
has_global_ssh_key: boolean;
|
||||
default_switchos_username: string | null;
|
||||
default_switchos_password: string | null;
|
||||
has_default_switchos_credentials: boolean;
|
||||
pushover_token: string | null;
|
||||
pushover_userkey: string | null;
|
||||
notify_failures_only: boolean;
|
||||
@@ -104,6 +107,8 @@ export class SettingsPageComponent implements OnInit, OnDestroy {
|
||||
enable_auto_export: false,
|
||||
connection_test_interval_minutes: [0, Validators.min(0)],
|
||||
global_ssh_key: '',
|
||||
default_switchos_username: '',
|
||||
default_switchos_password: '',
|
||||
pushover_token: '',
|
||||
pushover_userkey: '',
|
||||
notify_failures_only: true,
|
||||
@@ -376,6 +381,8 @@ export class SettingsPageComponent implements OnInit, OnDestroy {
|
||||
enable_auto_export: response.enable_auto_export,
|
||||
connection_test_interval_minutes: Number(response.connection_test_interval_minutes || 0),
|
||||
global_ssh_key: '',
|
||||
default_switchos_username: response.default_switchos_username || '',
|
||||
default_switchos_password: response.default_switchos_password || '',
|
||||
pushover_token: response.pushover_token || '',
|
||||
pushover_userkey: response.pushover_userkey || '',
|
||||
notify_failures_only: response.notify_failures_only,
|
||||
@@ -404,6 +411,8 @@ export class SettingsPageComponent implements OnInit, OnDestroy {
|
||||
enable_auto_export: Boolean(raw.enable_auto_export),
|
||||
connection_test_interval_minutes: Number(raw.connection_test_interval_minutes || 0),
|
||||
global_ssh_key: normalizedKey || null,
|
||||
default_switchos_username: this.normalizeOptionalText(raw.default_switchos_username),
|
||||
default_switchos_password: this.normalizeOptionalText(raw.default_switchos_password),
|
||||
pushover_token: this.normalizeOptionalText(raw.pushover_token),
|
||||
pushover_userkey: this.normalizeOptionalText(raw.pushover_userkey),
|
||||
notify_failures_only: Boolean(raw.notify_failures_only),
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
},
|
||||
"nav": {
|
||||
"dashboard": "Dashboard",
|
||||
"routers": "Routers",
|
||||
"routers": "Devices",
|
||||
"files": "Repository",
|
||||
"settings": "Settings",
|
||||
"logs": "Logs",
|
||||
@@ -81,7 +81,7 @@
|
||||
"subtitle": "Overview of backups, exports and operational activity in one place.",
|
||||
"exportAll": "Export all",
|
||||
"binaryAll": "Binary backup",
|
||||
"managedRouters": "Routers",
|
||||
"managedRouters": "Devices",
|
||||
"managedRoutersHint": "All managed devices",
|
||||
"inventoryTag": "Fleet",
|
||||
"exportsCard": "Exports",
|
||||
@@ -134,14 +134,14 @@
|
||||
"storageSnapshotHint": "Quick snapshot of the most important storage and backup indicators."
|
||||
},
|
||||
"routers": {
|
||||
"title": "Routers",
|
||||
"detailTitle": "Router details",
|
||||
"add": "Add router",
|
||||
"title": "Devices",
|
||||
"detailTitle": "Device details",
|
||||
"add": "Add device",
|
||||
"eyebrow": "device inventory",
|
||||
"subtitle": "Manage RouterOS endpoints, credentials and fleet-wide backup jobs.",
|
||||
"subtitle": "Manage RouterOS and SwitchOS devices plus their backups.",
|
||||
"registeredDevices": "Registered devices",
|
||||
"fleetTag": "Fleet",
|
||||
"sshPassword": "SSH password",
|
||||
"sshPassword": "Password",
|
||||
"passwordHint": "Password-based access",
|
||||
"credsTag": "Creds",
|
||||
"sshKey": "SSH key",
|
||||
@@ -150,8 +150,8 @@
|
||||
"defaultPort": "Port 22",
|
||||
"defaultPortHint": "Standard SSH endpoints",
|
||||
"portTag": "Port",
|
||||
"listTitle": "Router list",
|
||||
"listSubtitle": "Compact operational view of every managed device.",
|
||||
"listTitle": "Device list",
|
||||
"listSubtitle": "Unified view for RouterOS and SwitchOS devices.",
|
||||
"name": "Name",
|
||||
"endpoint": "Endpoint",
|
||||
"access": "Access",
|
||||
@@ -161,15 +161,15 @@
|
||||
"keyMode": "Key",
|
||||
"globalKeyMode": "Global key",
|
||||
"noKey": "No key",
|
||||
"createDialogTitle": "Add router",
|
||||
"editDialogTitle": "Edit router",
|
||||
"createDialogTitle": "Add device",
|
||||
"editDialogTitle": "Edit device",
|
||||
"host": "Host",
|
||||
"port": "Port",
|
||||
"sshUser": "SSH user",
|
||||
"sshUser": "Username",
|
||||
"sshPrivateKey": "SSH private key",
|
||||
"optionalPassword": "Optional password",
|
||||
"optionalPrivateKey": "Optional private key",
|
||||
"saveRouter": "Save router",
|
||||
"saveRouter": "Save device",
|
||||
"profileEyebrow": "router profile",
|
||||
"detailSubtitle": "Device operations and backup history",
|
||||
"exportOne": "Export",
|
||||
@@ -184,7 +184,7 @@
|
||||
"connectionLabelHint": "Status from the latest automatic or manual connection test",
|
||||
"probeTag": "Probe",
|
||||
"accessTag": "Access",
|
||||
"sshUserHint": "Current SSH user",
|
||||
"sshUserHint": "Effective device login",
|
||||
"deviceStatusTitle": "Device status",
|
||||
"deviceStatusSubtitle": "Stored metadata from the latest automatic or manual connection test.",
|
||||
"hostname": "Hostname",
|
||||
@@ -200,7 +200,7 @@
|
||||
"exportsTableTitle": "Exports",
|
||||
"exportsTableSubtitle": "Readable RouterOS snapshots.",
|
||||
"binaryTableTitle": "Binary backups",
|
||||
"binaryTableSubtitle": "Files ready for device restore.",
|
||||
"binaryTableSubtitle": "Binary files and SwitchOS backups.",
|
||||
"summaryKeyAccess": "with key-based access",
|
||||
"summaryPasswordAccess": "with password access",
|
||||
"connectionStateTitle": "Connection state",
|
||||
@@ -211,7 +211,28 @@
|
||||
"openPreviewModal": "Open preview",
|
||||
"diffModalHint": "The last loaded diff is available in a modal.",
|
||||
"openDiffModal": "Open diff",
|
||||
"noDiff": "Choose an export and run a diff to see the latest comparison."
|
||||
"noDiff": "Choose an export and run a diff to see the latest comparison.",
|
||||
"routeros": "RouterOS",
|
||||
"switchos": "SwitchOS",
|
||||
"deviceType": "Device type",
|
||||
"defaultCredentials": "Default credentials",
|
||||
"localCredentials": "Local credentials",
|
||||
"noCredentials": "No credentials",
|
||||
"switchUserPlaceholder": "Empty = use settings default",
|
||||
"switchPasswordPlaceholder": "Empty = use settings default",
|
||||
"switchDefaultsHint": "For SwitchOS you can leave username and password empty to use the defaults from settings.",
|
||||
"downloadSwitchBackup": "Download backup",
|
||||
"httpStatus": "HTTP status",
|
||||
"serverHeader": "Server header",
|
||||
"authMode": "Auth mode",
|
||||
"backupEndpoint": "Backup endpoint",
|
||||
"backupAvailable": "Available",
|
||||
"backupUnavailable": "Unavailable",
|
||||
"connectionSectionTitle": "Connection profile",
|
||||
"connectionSectionHint": "Basic device identity and endpoint used to reach it.",
|
||||
"credentialsSectionTitle": "Access and credentials",
|
||||
"routerDialogSubtitle": "Set the device endpoint, SSH access data and your preferred login method.",
|
||||
"switchDialogSubtitle": "Set the SwitchOS endpoint and optional local or shared credentials from settings."
|
||||
},
|
||||
"files": {
|
||||
"title": "Repository",
|
||||
@@ -233,14 +254,14 @@
|
||||
"searchLabel": "Search",
|
||||
"searchPlaceholder": "Search by file or router",
|
||||
"typeLabel": "Type",
|
||||
"routerLabel": "Router",
|
||||
"routerLabel": "Device",
|
||||
"sortLabel": "Sort by",
|
||||
"orderLabel": "Order",
|
||||
"allTypes": "All types",
|
||||
"allRouters": "All routers",
|
||||
"allRouters": "All devices",
|
||||
"sortNewest": "Newest",
|
||||
"sortName": "Name",
|
||||
"sortRouter": "Router",
|
||||
"sortRouter": "Device",
|
||||
"sortType": "Type",
|
||||
"tableTitle": "Repository table",
|
||||
"tableSubtitle": "Artifacts available for download, e-mail and restore.",
|
||||
@@ -248,7 +269,7 @@
|
||||
"compareSelected": "Compare selected exports",
|
||||
"fileColumn": "File",
|
||||
"typeColumn": "Type",
|
||||
"routerColumn": "Router",
|
||||
"routerColumn": "Device",
|
||||
"createdColumn": "Created",
|
||||
"actionsColumn": "Actions",
|
||||
"checksum": "Checksum",
|
||||
@@ -311,8 +332,8 @@
|
||||
"pushoverUserKey": "Pushover user key",
|
||||
"pushoverTokenPlaceholder": "Application token",
|
||||
"pushoverUserKeyPlaceholder": "User key",
|
||||
"sshDefaultsTitle": "SSH defaults",
|
||||
"sshDefaultsSubtitle": "Optional shared private key used across managed routers.",
|
||||
"sshDefaultsTitle": "Default Credentials",
|
||||
"sshDefaultsSubtitle": "Shared SSH key and default SwitchOS login used across managed devices.",
|
||||
"globalSshPrivateKey": "Global SSH private key",
|
||||
"globalSshPrivateKeyPlaceholder": "Paste PEM or OpenSSH private key",
|
||||
"save": "Save settings",
|
||||
@@ -377,7 +398,11 @@
|
||||
"interfacePreferencesHint": "Choose the default language and font family for the whole application.",
|
||||
"interfacePreferencesTag": "Per-user",
|
||||
"fontFamily": "Font family",
|
||||
"fontDefault": "Default"
|
||||
"fontDefault": "Default",
|
||||
"switchosDefaultsTitle": "Default SwitchOS credentials",
|
||||
"switchosDefaultsHint": "Used when a SwitchOS device has no local username or password.",
|
||||
"defaultSwitchosUsername": "Default SwitchOS username",
|
||||
"defaultSwitchosPassword": "Default SwitchOS password"
|
||||
},
|
||||
"logs": {
|
||||
"title": "Logs",
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
},
|
||||
"nav": {
|
||||
"dashboard": "Panel",
|
||||
"routers": "Routers",
|
||||
"routers": "Dispositivos",
|
||||
"files": "Repositorio",
|
||||
"settings": "Ajustes",
|
||||
"logs": "Registros",
|
||||
@@ -81,7 +81,7 @@
|
||||
"subtitle": "Resumen de copias, exportaciones y actividad operativa en un solo lugar.",
|
||||
"exportAll": "Exportar todo",
|
||||
"binaryAll": "Copia binaria",
|
||||
"managedRouters": "Routers",
|
||||
"managedRouters": "Dispositivos",
|
||||
"managedRoutersHint": "Todos los dispositivos gestionados",
|
||||
"inventoryTag": "Flota",
|
||||
"exportsCard": "Exportaciones",
|
||||
@@ -134,14 +134,14 @@
|
||||
"storageSnapshotHint": "Vista rápida de los indicadores más importantes de almacenamiento y copias."
|
||||
},
|
||||
"routers": {
|
||||
"title": "Routers",
|
||||
"detailTitle": "Detalles del router",
|
||||
"add": "Añadir router",
|
||||
"title": "Dispositivos",
|
||||
"detailTitle": "Detalles del dispositivo",
|
||||
"add": "Agregar dispositivo",
|
||||
"eyebrow": "inventario de dispositivos",
|
||||
"subtitle": "Gestiona endpoints de RouterOS, credenciales y tareas de copia para toda la flota.",
|
||||
"subtitle": "Administra dispositivos RouterOS y SwitchOS y sus copias.",
|
||||
"registeredDevices": "Dispositivos registrados",
|
||||
"fleetTag": "Flota",
|
||||
"sshPassword": "Contraseña SSH",
|
||||
"sshPassword": "Contraseña",
|
||||
"passwordHint": "Acceso con contraseña",
|
||||
"credsTag": "Credenciales",
|
||||
"sshKey": "Clave SSH",
|
||||
@@ -150,8 +150,8 @@
|
||||
"defaultPort": "Puerto 22",
|
||||
"defaultPortHint": "Endpoints SSH estándar",
|
||||
"portTag": "Puerto",
|
||||
"listTitle": "Lista de routers",
|
||||
"listSubtitle": "Vista operativa compacta de todos los dispositivos gestionados.",
|
||||
"listTitle": "Lista de dispositivos",
|
||||
"listSubtitle": "Vista unificada para RouterOS y SwitchOS.",
|
||||
"name": "Nombre",
|
||||
"endpoint": "Endpoint",
|
||||
"access": "Acceso",
|
||||
@@ -161,15 +161,15 @@
|
||||
"keyMode": "Clave",
|
||||
"globalKeyMode": "Clave global",
|
||||
"noKey": "Sin clave",
|
||||
"createDialogTitle": "Añadir router",
|
||||
"editDialogTitle": "Editar router",
|
||||
"createDialogTitle": "Agregar dispositivo",
|
||||
"editDialogTitle": "Editar dispositivo",
|
||||
"host": "Host",
|
||||
"port": "Puerto",
|
||||
"sshUser": "Usuario SSH",
|
||||
"sshUser": "Usuario",
|
||||
"sshPrivateKey": "Clave privada SSH",
|
||||
"optionalPassword": "Contraseña opcional",
|
||||
"optionalPrivateKey": "Clave privada opcional",
|
||||
"saveRouter": "Guardar router",
|
||||
"saveRouter": "Guardar dispositivo",
|
||||
"profileEyebrow": "perfil del router",
|
||||
"detailSubtitle": "Operaciones del dispositivo e historial de copias",
|
||||
"exportOne": "Exportar",
|
||||
@@ -211,7 +211,28 @@
|
||||
"openPreviewModal": "Abrir vista previa",
|
||||
"diffModalHint": "El último diff cargado está disponible en un modal.",
|
||||
"openDiffModal": "Abrir diff",
|
||||
"noDiff": "Elige una exportación y ejecuta un diff para ver la última comparación."
|
||||
"noDiff": "Elige una exportación y ejecuta un diff para ver la última comparación.",
|
||||
"routeros": "RouterOS",
|
||||
"switchos": "SwitchOS",
|
||||
"deviceType": "Tipo de dispositivo",
|
||||
"defaultCredentials": "Credenciales por defecto",
|
||||
"localCredentials": "Credenciales locales",
|
||||
"noCredentials": "Sin credenciales",
|
||||
"switchUserPlaceholder": "Vacío = usar ajustes",
|
||||
"switchPasswordPlaceholder": "Vacío = usar ajustes",
|
||||
"switchDefaultsHint": "Para SwitchOS puedes dejar usuario y contraseña vacíos para usar los valores por defecto.",
|
||||
"downloadSwitchBackup": "Descargar copia",
|
||||
"httpStatus": "Estado HTTP",
|
||||
"serverHeader": "Cabecera Server",
|
||||
"authMode": "Modo de autenticación",
|
||||
"backupEndpoint": "Endpoint de copia",
|
||||
"backupAvailable": "Disponible",
|
||||
"backupUnavailable": "No disponible",
|
||||
"connectionSectionTitle": "Perfil de conexión",
|
||||
"connectionSectionHint": "Identidad básica del dispositivo y endpoint usado para alcanzarlo.",
|
||||
"credentialsSectionTitle": "Acceso y credenciales",
|
||||
"routerDialogSubtitle": "Configura el endpoint del dispositivo, los datos SSH y el método de acceso preferido.",
|
||||
"switchDialogSubtitle": "Configura el endpoint de SwitchOS y las credenciales locales u opcionales compartidas desde ajustes."
|
||||
},
|
||||
"files": {
|
||||
"title": "Repositorio",
|
||||
@@ -233,14 +254,14 @@
|
||||
"searchLabel": "Buscar",
|
||||
"searchPlaceholder": "Buscar por archivo o router",
|
||||
"typeLabel": "Tipo",
|
||||
"routerLabel": "Router",
|
||||
"routerLabel": "Dispositivo",
|
||||
"sortLabel": "Ordenar por",
|
||||
"orderLabel": "Orden",
|
||||
"allTypes": "Todos los tipos",
|
||||
"allRouters": "Todos los routers",
|
||||
"allRouters": "Todos los dispositivos",
|
||||
"sortNewest": "Más nuevo",
|
||||
"sortName": "Nombre",
|
||||
"sortRouter": "Router",
|
||||
"sortRouter": "Dispositivo",
|
||||
"sortType": "Tipo",
|
||||
"tableTitle": "Tabla del repositorio",
|
||||
"tableSubtitle": "Artefactos disponibles para descarga, correo y restauración.",
|
||||
@@ -248,7 +269,7 @@
|
||||
"compareSelected": "Comparar exportaciones seleccionadas",
|
||||
"fileColumn": "Archivo",
|
||||
"typeColumn": "Tipo",
|
||||
"routerColumn": "Router",
|
||||
"routerColumn": "Dispositivo",
|
||||
"createdColumn": "Creado",
|
||||
"actionsColumn": "Acciones",
|
||||
"checksum": "Checksum",
|
||||
@@ -311,8 +332,8 @@
|
||||
"pushoverUserKey": "Clave de usuario de Pushover",
|
||||
"pushoverTokenPlaceholder": "Token de la aplicación",
|
||||
"pushoverUserKeyPlaceholder": "Clave de usuario",
|
||||
"sshDefaultsTitle": "Valores SSH por defecto",
|
||||
"sshDefaultsSubtitle": "Clave privada compartida opcional usada en todos los routers gestionados.",
|
||||
"sshDefaultsTitle": "Credenciales predeterminadas",
|
||||
"sshDefaultsSubtitle": "Clave SSH compartida y acceso por defecto de SwitchOS usados por los dispositivos gestionados.",
|
||||
"globalSshPrivateKey": "Clave privada SSH global",
|
||||
"globalSshPrivateKeyPlaceholder": "Pega la clave privada PEM u OpenSSH",
|
||||
"globalSshPrivateKeyHiddenPlaceholder": "La clave guardada está oculta. Introduce la contraseña arriba para verla o pega aquí una nueva clave para reemplazarla.",
|
||||
@@ -377,7 +398,11 @@
|
||||
"interfacePreferencesHint": "Elige el idioma predeterminado y la familia tipográfica para toda la aplicación.",
|
||||
"interfacePreferencesTag": "Por usuario",
|
||||
"fontFamily": "Familia tipográfica",
|
||||
"fontDefault": "Predeterminada"
|
||||
"fontDefault": "Predeterminada",
|
||||
"switchosDefaultsTitle": "Credenciales por defecto de SwitchOS",
|
||||
"switchosDefaultsHint": "Se usan cuando un dispositivo SwitchOS no tiene usuario o contraseña local.",
|
||||
"defaultSwitchosUsername": "Usuario SwitchOS por defecto",
|
||||
"defaultSwitchosPassword": "Contraseña SwitchOS por defecto"
|
||||
},
|
||||
"logs": {
|
||||
"title": "Registros",
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
},
|
||||
"nav": {
|
||||
"dashboard": "Dashbord",
|
||||
"routers": "Rutere",
|
||||
"routers": "Enheter",
|
||||
"files": "Repository",
|
||||
"settings": "Innstillinger",
|
||||
"logs": "Logger",
|
||||
@@ -81,7 +81,7 @@
|
||||
"subtitle": "Oversikt over backuper, eksportfiler og operativ aktivitet på ett sted.",
|
||||
"exportAll": "Eksporter alle",
|
||||
"binaryAll": "Binær backup",
|
||||
"managedRouters": "Rutere",
|
||||
"managedRouters": "Enheter",
|
||||
"managedRoutersHint": "Alle administrerte enheter",
|
||||
"inventoryTag": "Flåte",
|
||||
"exportsCard": "Eksporter",
|
||||
@@ -134,14 +134,14 @@
|
||||
"storageSnapshotHint": "Rask oversikt over de viktigste lagrings- og backupindikatorene."
|
||||
},
|
||||
"routers": {
|
||||
"title": "Rutere",
|
||||
"detailTitle": "Ruterdetaljer",
|
||||
"add": "Legg til ruter",
|
||||
"title": "Enheter",
|
||||
"detailTitle": "Enhetsdetaljer",
|
||||
"add": "Legg til enhet",
|
||||
"eyebrow": "enhetsinventar",
|
||||
"subtitle": "Administrer RouterOS-endepunkter, legitimasjon og backupjobber for hele flåten.",
|
||||
"subtitle": "Administrer RouterOS- og SwitchOS-enheter og sikkerhetskopier.",
|
||||
"registeredDevices": "Registrerte enheter",
|
||||
"fleetTag": "Flåte",
|
||||
"sshPassword": "SSH-passord",
|
||||
"sshPassword": "Passord",
|
||||
"passwordHint": "Passordbasert tilgang",
|
||||
"credsTag": "Tilgang",
|
||||
"sshKey": "SSH-nøkkel",
|
||||
@@ -150,8 +150,8 @@
|
||||
"defaultPort": "Port 22",
|
||||
"defaultPortHint": "Standard SSH-endepunkter",
|
||||
"portTag": "Port",
|
||||
"listTitle": "Ruterliste",
|
||||
"listSubtitle": "Kompakt driftsvisning av alle administrerte enheter.",
|
||||
"listTitle": "Enhetsliste",
|
||||
"listSubtitle": "Felles visning for RouterOS og SwitchOS.",
|
||||
"name": "Navn",
|
||||
"endpoint": "Endepunkt",
|
||||
"access": "Tilgang",
|
||||
@@ -161,15 +161,15 @@
|
||||
"keyMode": "Nøkkel",
|
||||
"globalKeyMode": "Global nøkkel",
|
||||
"noKey": "Ingen nøkkel",
|
||||
"createDialogTitle": "Legg til ruter",
|
||||
"editDialogTitle": "Rediger ruter",
|
||||
"createDialogTitle": "Legg til enhet",
|
||||
"editDialogTitle": "Rediger enhet",
|
||||
"host": "Vert",
|
||||
"port": "Port",
|
||||
"sshUser": "SSH-bruker",
|
||||
"sshUser": "Bruker",
|
||||
"sshPrivateKey": "SSH privat nøkkel",
|
||||
"optionalPassword": "Valgfritt passord",
|
||||
"optionalPrivateKey": "Valgfri privat nøkkel",
|
||||
"saveRouter": "Lagre ruter",
|
||||
"saveRouter": "Lagre enhet",
|
||||
"profileEyebrow": "ruterprofil",
|
||||
"detailSubtitle": "Enhetsoperasjoner og backuphistorikk",
|
||||
"exportOne": "Eksport",
|
||||
@@ -211,7 +211,28 @@
|
||||
"openPreviewModal": "Åpne forhåndsvisning",
|
||||
"diffModalHint": "Sist lastede diff er tilgjengelig i en modal.",
|
||||
"openDiffModal": "Åpne diff",
|
||||
"noDiff": "Velg en eksport og kjør diff for å se siste sammenligning."
|
||||
"noDiff": "Velg en eksport og kjør diff for å se siste sammenligning.",
|
||||
"routeros": "RouterOS",
|
||||
"switchos": "SwitchOS",
|
||||
"deviceType": "Enhetstype",
|
||||
"defaultCredentials": "Standard legitimasjon",
|
||||
"localCredentials": "Lokal legitimasjon",
|
||||
"noCredentials": "Ingen legitimasjon",
|
||||
"switchUserPlaceholder": "Tom = bruk innstillinger",
|
||||
"switchPasswordPlaceholder": "Tom = bruk innstillinger",
|
||||
"switchDefaultsHint": "For SwitchOS kan du la bruker og passord være tomme for å bruke standardverdier fra innstillinger.",
|
||||
"downloadSwitchBackup": "Last ned backup",
|
||||
"httpStatus": "HTTP-status",
|
||||
"serverHeader": "Server-header",
|
||||
"authMode": "Autentiseringsmodus",
|
||||
"backupEndpoint": "Backup-endepunkt",
|
||||
"backupAvailable": "Tilgjengelig",
|
||||
"backupUnavailable": "Utilgjengelig",
|
||||
"connectionSectionTitle": "Tilkoblingsprofil",
|
||||
"connectionSectionHint": "Grunnleggende enhetsidentitet og endpoint som brukes for å nå den.",
|
||||
"credentialsSectionTitle": "Tilgang og legitimasjon",
|
||||
"routerDialogSubtitle": "Sett enhetens endpoint, SSH-data og foretrukket innloggingsmetode.",
|
||||
"switchDialogSubtitle": "Sett SwitchOS-endpoint og valgfrie lokale eller delte standarddata fra innstillinger."
|
||||
},
|
||||
"files": {
|
||||
"title": "Repository",
|
||||
@@ -233,14 +254,14 @@
|
||||
"searchLabel": "Søk",
|
||||
"searchPlaceholder": "Søk etter fil eller ruter",
|
||||
"typeLabel": "Type",
|
||||
"routerLabel": "Ruter",
|
||||
"routerLabel": "Enhet",
|
||||
"sortLabel": "Sorter etter",
|
||||
"orderLabel": "Rekkefølge",
|
||||
"allTypes": "Alle typer",
|
||||
"allRouters": "Alle rutere",
|
||||
"allRouters": "Alle enheter",
|
||||
"sortNewest": "Nyeste",
|
||||
"sortName": "Navn",
|
||||
"sortRouter": "Ruter",
|
||||
"sortRouter": "Enhet",
|
||||
"sortType": "Type",
|
||||
"tableTitle": "Repositorytabell",
|
||||
"tableSubtitle": "Artefakter tilgjengelige for nedlasting, e-post og gjenoppretting.",
|
||||
@@ -248,7 +269,7 @@
|
||||
"compareSelected": "Sammenlign valgte eksporter",
|
||||
"fileColumn": "Fil",
|
||||
"typeColumn": "Type",
|
||||
"routerColumn": "Ruter",
|
||||
"routerColumn": "Enhet",
|
||||
"createdColumn": "Opprettet",
|
||||
"actionsColumn": "Handlinger",
|
||||
"checksum": "Checksum",
|
||||
@@ -311,8 +332,8 @@
|
||||
"pushoverUserKey": "Pushover-brukernøkkel",
|
||||
"pushoverTokenPlaceholder": "Applikasjonstoken",
|
||||
"pushoverUserKeyPlaceholder": "Brukernøkkel",
|
||||
"sshDefaultsTitle": "SSH-standarder",
|
||||
"sshDefaultsSubtitle": "Valgfri delt privat nøkkel som brukes på tvers av administrerte rutere.",
|
||||
"sshDefaultsTitle": "Standard legitimasjon",
|
||||
"sshDefaultsSubtitle": "Delt SSH-nøkkel og standard innlogging for SwitchOS brukt på administrerte enheter.",
|
||||
"globalSshPrivateKey": "Global SSH privat nøkkel",
|
||||
"globalSshPrivateKeyPlaceholder": "Lim inn PEM- eller OpenSSH-privat nøkkel",
|
||||
"globalSshPrivateKeyHiddenPlaceholder": "Den lagrede nøkkelen er skjult. Skriv inn passordet over for å se den, eller lim inn en ny nøkkel her for å erstatte den.",
|
||||
@@ -377,7 +398,11 @@
|
||||
"interfacePreferencesHint": "Velg standardspråk og skriftfamilie for hele applikasjonen.",
|
||||
"interfacePreferencesTag": "Per bruker",
|
||||
"fontFamily": "Skriftfamilie",
|
||||
"fontDefault": "Standard"
|
||||
"fontDefault": "Standard",
|
||||
"switchosDefaultsTitle": "Standard SwitchOS-legitimasjon",
|
||||
"switchosDefaultsHint": "Brukes når en SwitchOS-enhet ikke har lokalt brukernavn eller passord.",
|
||||
"defaultSwitchosUsername": "Standard SwitchOS-bruker",
|
||||
"defaultSwitchosPassword": "Standard SwitchOS-passord"
|
||||
},
|
||||
"logs": {
|
||||
"title": "Logger",
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
},
|
||||
"nav": {
|
||||
"dashboard": "Dashboard",
|
||||
"routers": "Routery",
|
||||
"routers": "Urządzenia",
|
||||
"files": "Repozytorium",
|
||||
"settings": "Ustawienia",
|
||||
"logs": "Logi",
|
||||
@@ -81,7 +81,7 @@
|
||||
"subtitle": "Przegląd backupów, eksportów i aktywności operacyjnej w jednym miejscu.",
|
||||
"exportAll": "Eksportuj wszystko",
|
||||
"binaryAll": "Backup binarny",
|
||||
"managedRouters": "Routery",
|
||||
"managedRouters": "Urządzenia",
|
||||
"managedRoutersHint": "Wszystkie zarządzane urządzenia",
|
||||
"inventoryTag": "Flota",
|
||||
"exportsCard": "Eksporty",
|
||||
@@ -134,14 +134,14 @@
|
||||
"storageSnapshotHint": "Szybki podgląd najważniejszych wskaźników przestrzeni i backupów."
|
||||
},
|
||||
"routers": {
|
||||
"title": "Routery",
|
||||
"detailTitle": "Szczegóły routera",
|
||||
"add": "Dodaj router",
|
||||
"title": "Urządzenia",
|
||||
"detailTitle": "Szczegóły urządzenia",
|
||||
"add": "Dodaj urządzenie",
|
||||
"eyebrow": "inwentaryzacja urządzeń",
|
||||
"subtitle": "Zarządzaj endpointami RouterOS, poświadczeniami i zadaniami backupu dla całej floty.",
|
||||
"subtitle": "Zarządzaj urządzeniami RouterOS i SwitchOS oraz ich kopiami.",
|
||||
"registeredDevices": "Zarejestrowane urządzenia",
|
||||
"fleetTag": "Flota",
|
||||
"sshPassword": "Hasło SSH",
|
||||
"sshPassword": "Hasło",
|
||||
"passwordHint": "Dostęp hasłem",
|
||||
"credsTag": "Dostęp",
|
||||
"sshKey": "Klucz SSH",
|
||||
@@ -150,8 +150,8 @@
|
||||
"defaultPort": "Port 22",
|
||||
"defaultPortHint": "Standardowe endpointy SSH",
|
||||
"portTag": "Port",
|
||||
"listTitle": "Lista routerów",
|
||||
"listSubtitle": "Zwięzły widok operacyjny wszystkich zarządzanych urządzeń.",
|
||||
"listTitle": "Lista urządzeń",
|
||||
"listSubtitle": "Wspólny widok RouterOS i SwitchOS.",
|
||||
"name": "Nazwa",
|
||||
"endpoint": "Endpoint",
|
||||
"access": "Dostęp",
|
||||
@@ -161,15 +161,15 @@
|
||||
"keyMode": "Klucz",
|
||||
"globalKeyMode": "Klucz globalny",
|
||||
"noKey": "Bez klucza",
|
||||
"createDialogTitle": "Dodaj router",
|
||||
"editDialogTitle": "Edytuj router",
|
||||
"createDialogTitle": "Dodaj urządzenie",
|
||||
"editDialogTitle": "Edytuj urządzenie",
|
||||
"host": "Host",
|
||||
"port": "Port",
|
||||
"sshUser": "Użytkownik SSH",
|
||||
"sshUser": "Użytkownik",
|
||||
"sshPrivateKey": "Klucz prywatny SSH",
|
||||
"optionalPassword": "Opcjonalne hasło",
|
||||
"optionalPrivateKey": "Opcjonalny klucz prywatny",
|
||||
"saveRouter": "Zapisz router",
|
||||
"saveRouter": "Zapisz urządzenie",
|
||||
"profileEyebrow": "profil routera",
|
||||
"detailSubtitle": "Operacje urządzenia i historia backupów",
|
||||
"exportOne": "Eksport",
|
||||
@@ -184,7 +184,7 @@
|
||||
"connectionLabelHint": "Status z ostatniego automatycznego lub ręcznego testu połączenia",
|
||||
"probeTag": "Test",
|
||||
"accessTag": "Dostęp",
|
||||
"sshUserHint": "Bieżący użytkownik SSH",
|
||||
"sshUserHint": "Efektywny login urządzenia",
|
||||
"deviceStatusTitle": "Status urządzenia",
|
||||
"deviceStatusSubtitle": "Zapisane metadane z ostatniego automatycznego lub ręcznego testu połączenia.",
|
||||
"hostname": "Hostname",
|
||||
@@ -200,7 +200,7 @@
|
||||
"exportsTableTitle": "Eksporty",
|
||||
"exportsTableSubtitle": "Czytelne snapshoty RouterOS.",
|
||||
"binaryTableTitle": "Backupy binarne",
|
||||
"binaryTableSubtitle": "Pliki do odtworzenia urządzenia.",
|
||||
"binaryTableSubtitle": "Pliki binarne i kopie SwitchOS.",
|
||||
"summaryKeyAccess": "z dostępem kluczem",
|
||||
"summaryPasswordAccess": "z dostępem hasłem",
|
||||
"connectionStateTitle": "Stan połączenia",
|
||||
@@ -211,7 +211,28 @@
|
||||
"openPreviewModal": "Otwórz podgląd",
|
||||
"diffModalHint": "Ostatnio załadowany diff jest dostępny w modalu.",
|
||||
"openDiffModal": "Otwórz diff",
|
||||
"noDiff": "Wybierz eksport i uruchom diff, aby zobaczyć ostatnie porównanie."
|
||||
"noDiff": "Wybierz eksport i uruchom diff, aby zobaczyć ostatnie porównanie.",
|
||||
"routeros": "RouterOS",
|
||||
"switchos": "SwitchOS",
|
||||
"deviceType": "Typ urządzenia",
|
||||
"defaultCredentials": "Domyślne dane",
|
||||
"localCredentials": "Lokalne dane",
|
||||
"noCredentials": "Brak danych",
|
||||
"switchUserPlaceholder": "Puste = z ustawień",
|
||||
"switchPasswordPlaceholder": "Puste = z ustawień",
|
||||
"switchDefaultsHint": "Dla SwitchOS możesz zostawić użytkownika i hasło puste, aby użyć wartości domyślnych z ustawień.",
|
||||
"downloadSwitchBackup": "Pobierz backup",
|
||||
"httpStatus": "Status HTTP",
|
||||
"serverHeader": "Nagłówek Server",
|
||||
"authMode": "Tryb autoryzacji",
|
||||
"backupEndpoint": "Endpoint backupu",
|
||||
"backupAvailable": "Dostępny",
|
||||
"backupUnavailable": "Niedostępny",
|
||||
"connectionSectionTitle": "Profil połączenia",
|
||||
"connectionSectionHint": "Podstawowa tożsamość urządzenia i endpoint używany do połączenia.",
|
||||
"credentialsSectionTitle": "Dostęp i poświadczenia",
|
||||
"routerDialogSubtitle": "Ustaw adres urządzenia, dane dostępu SSH i preferowaną metodę logowania.",
|
||||
"switchDialogSubtitle": "Ustaw endpoint SwitchOS i opcjonalne poświadczenia lokalne lub domyślne z ustawień."
|
||||
},
|
||||
"files": {
|
||||
"title": "Repozytorium",
|
||||
@@ -233,14 +254,14 @@
|
||||
"searchLabel": "Szukaj",
|
||||
"searchPlaceholder": "Szukaj po pliku lub routerze",
|
||||
"typeLabel": "Typ",
|
||||
"routerLabel": "Router",
|
||||
"routerLabel": "Urządzenie",
|
||||
"sortLabel": "Sortowanie",
|
||||
"orderLabel": "Kolejność",
|
||||
"allTypes": "Wszystkie typy",
|
||||
"allRouters": "Wszystkie routery",
|
||||
"allRouters": "Wszystkie urządzenia",
|
||||
"sortNewest": "Najnowsze",
|
||||
"sortName": "Nazwa",
|
||||
"sortRouter": "Router",
|
||||
"sortRouter": "Urządzenie",
|
||||
"sortType": "Typ",
|
||||
"tableTitle": "Tabela repozytorium",
|
||||
"tableSubtitle": "Artefakty dostępne do pobrania, wysyłki e-mail i przywracania.",
|
||||
@@ -248,7 +269,7 @@
|
||||
"compareSelected": "Porównaj zaznaczone eksporty",
|
||||
"fileColumn": "Plik",
|
||||
"typeColumn": "Typ",
|
||||
"routerColumn": "Router",
|
||||
"routerColumn": "Urządzenie",
|
||||
"createdColumn": "Utworzono",
|
||||
"actionsColumn": "Akcje",
|
||||
"checksum": "Checksum",
|
||||
@@ -311,8 +332,8 @@
|
||||
"pushoverUserKey": "Klucz użytkownika Pushover",
|
||||
"pushoverTokenPlaceholder": "Token aplikacji",
|
||||
"pushoverUserKeyPlaceholder": "Klucz użytkownika",
|
||||
"sshDefaultsTitle": "Domyślne SSH",
|
||||
"sshDefaultsSubtitle": "Opcjonalny współdzielony klucz prywatny używany przez zarządzane routery.",
|
||||
"sshDefaultsTitle": "Domyślne Poświadczenia",
|
||||
"sshDefaultsSubtitle": "Wspólny klucz SSH oraz domyślne logowanie SwitchOS używane przez urządzenia.",
|
||||
"globalSshPrivateKey": "Globalny klucz prywatny SSH",
|
||||
"globalSshPrivateKeyPlaceholder": "Wklej klucz prywatny PEM lub OpenSSH",
|
||||
"save": "Zapisz ustawienia",
|
||||
@@ -377,7 +398,11 @@
|
||||
"interfacePreferencesHint": "Wybierz domyślny język i rodzinę fontów dla całej aplikacji.",
|
||||
"interfacePreferencesTag": "Per-user",
|
||||
"fontFamily": "Rodzina fontów",
|
||||
"fontDefault": "Domyślna"
|
||||
"fontDefault": "Domyślna",
|
||||
"switchosDefaultsTitle": "Domyślne dane SwitchOS",
|
||||
"switchosDefaultsHint": "Używane, gdy urządzenie SwitchOS nie ma własnego loginu lub hasła.",
|
||||
"defaultSwitchosUsername": "Domyślny użytkownik SwitchOS",
|
||||
"defaultSwitchosPassword": "Domyślne hasło SwitchOS"
|
||||
},
|
||||
"logs": {
|
||||
"title": "Logi",
|
||||
|
||||
@@ -3389,3 +3389,198 @@ body.dark-theme .p-confirm-dialog .p-confirm-dialog-icon{
|
||||
@media (max-width: 991px) {
|
||||
|
||||
}
|
||||
|
||||
.router-dialog .p-dialog-header{
|
||||
padding: 1.15rem 1.2rem 1rem;
|
||||
align-items: flex-start;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(75, 144, 217, 0.16), rgba(79, 181, 147, 0.1)),
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0)),
|
||||
var(--surface-1);
|
||||
border-bottom: 1px solid rgba(75, 144, 217, 0.18);
|
||||
}
|
||||
|
||||
.router-dialog .p-dialog-header-icons{
|
||||
align-self: flex-start;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.router-dialog .p-dialog-content{
|
||||
padding: 0 1.2rem 1.2rem;
|
||||
background: linear-gradient(180deg, rgba(75, 144, 217, 0.06) 0%, rgba(75, 144, 217, 0) 180px), var(--surface-1);
|
||||
}
|
||||
|
||||
.router-dialog-header{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.9rem;
|
||||
width: calc(100% - 0.5rem);
|
||||
}
|
||||
|
||||
.router-dialog-header__icon{
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 18px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex-shrink: 0;
|
||||
background: linear-gradient(135deg, rgba(75, 144, 217, 0.24), rgba(79, 181, 147, 0.14));
|
||||
border: 1px solid rgba(75, 144, 217, 0.2);
|
||||
color: var(--primary);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.router-dialog-header__icon .pi{
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.router-dialog-header__text{
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.router-dialog-header__eyebrow{
|
||||
font-family: var(--font-title);
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-soft);
|
||||
}
|
||||
|
||||
.router-dialog-header__title{
|
||||
margin-top: 0.2rem;
|
||||
font-family: var(--font-title);
|
||||
font-size: 1.18rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.router-dialog-header__text small{
|
||||
display: block;
|
||||
margin-top: 0.3rem;
|
||||
max-width: 42rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.router-dialog-form{
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.router-dialog-panel{
|
||||
padding: 1rem;
|
||||
border-radius: 22px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0)), var(--surface-0);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.router-dialog-panel:first-child{
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.router-dialog-panel__header{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.95rem;
|
||||
}
|
||||
|
||||
.router-dialog-panel__header strong{
|
||||
display: block;
|
||||
font-family: var(--font-title);
|
||||
font-size: 0.88rem;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.router-dialog-panel__header p{
|
||||
margin: 0.35rem 0 0;
|
||||
color: var(--text-soft);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.router-dialog-pill{
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
padding: 0.55rem 0.85rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(75, 144, 217, 0.18);
|
||||
background: rgba(75, 144, 217, 0.08);
|
||||
color: var(--text-main);
|
||||
font-family: var(--font-title);
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.router-dialog-grid{
|
||||
gap: 0.95rem 1rem;
|
||||
}
|
||||
|
||||
.router-dialog-note{
|
||||
margin-top: 0.9rem;
|
||||
padding: 0.85rem 0.95rem;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(75, 144, 217, 0.18);
|
||||
background: rgba(75, 144, 217, 0.08);
|
||||
color: var(--text-soft);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.65rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.router-dialog-note .pi{
|
||||
margin-top: 0.1rem;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.router-dialog .p-inputtext,
|
||||
.router-dialog .p-dropdown,
|
||||
.router-dialog .p-inputtextarea{
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.router-dialog .p-inputtextarea{
|
||||
min-height: 11rem;
|
||||
}
|
||||
|
||||
.router-dialog-actions{
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
padding-top: 0.1rem;
|
||||
}
|
||||
|
||||
.router-dialog-actions .p-button{
|
||||
min-width: 11rem;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.router-dialog .p-dialog-header{
|
||||
padding: 1rem 0.85rem 0.85rem;
|
||||
}
|
||||
|
||||
.router-dialog .p-dialog-content{
|
||||
padding: 0 0.85rem 0.95rem;
|
||||
}
|
||||
|
||||
.router-dialog-header{
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.router-dialog-panel__header{
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.router-dialog-actions{
|
||||
flex-direction: column-reverse;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.router-dialog-actions .p-button{
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user