diff --git a/backend/app/api/routes/backups.py b/backend/app/api/routes/backups.py index 16e6a02..991bb2c 100644 --- a/backend/app/api/routes/backups.py +++ b/backend/app/api/routes/backups.py @@ -1,3 +1,4 @@ +from datetime import date import io import zipfile @@ -18,6 +19,7 @@ def list_backups( search: str | None = Query(default=None), backup_type: str | None = Query(default=None, pattern="^(export|binary)$"), router_id: int | None = Query(default=None), + created_on: date | None = Query(default=None), sort_by: str = Query(default="created_at"), order: str = Query(default="desc", pattern="^(asc|desc)$"), current_user: User = Depends(get_current_user), @@ -29,6 +31,7 @@ def list_backups( search=search, backup_type=backup_type, router_id=router_id, + created_on=created_on, sort_by=sort_by, order=order, ) diff --git a/backend/app/services/backup_service.py b/backend/app/services/backup_service.py index 856906f..7522aef 100644 --- a/backend/app/services/backup_service.py +++ b/backend/app/services/backup_service.py @@ -1,5 +1,5 @@ import difflib -from datetime import datetime, timedelta, timezone +from datetime import date, datetime, time, timedelta, timezone from pathlib import Path from fastapi import HTTPException @@ -146,6 +146,7 @@ class BackupService: search: str | None = None, backup_type: str | None = None, router_id: int | None = None, + created_on: date | None = None, sort_by: str = 'created_at', order: str = 'desc', ): @@ -160,6 +161,10 @@ class BackupService: query = query.filter(Backup.backup_type == backup_type) if router_id: query = query.filter(Backup.router_id == router_id) + if created_on: + day_start = datetime.combine(created_on, time.min) + next_day = day_start + timedelta(days=1) + query = query.filter(Backup.created_at >= day_start, Backup.created_at < next_day) sort_map = { 'created_at': Backup.created_at, diff --git a/backend/app/services/router_service.py b/backend/app/services/router_service.py index f1f4dcf..c682b8d 100644 --- a/backend/app/services/router_service.py +++ b/backend/app/services/router_service.py @@ -173,14 +173,35 @@ class RouterService: auth_mode = result.get('auth_mode') http_status = result.get('http_status') backup_available = result.get('backup_available') + hostname = result.get('hostname') + model = result.get('model') + version = result.get('version') + uptime = result.get('uptime') + server = result.get('server') - details = [f'via {transport}'] - if auth_mode: - details.append(f'auth={auth_mode}') - if http_status: - details.append(f'http={http_status}') - if backup_available is not None: - details.append(f'backup_available={"yes" if backup_available else "no"}') + details = [f'via {transport}', f'target={router.host}:{router.port}'] + if router.device_type == 'routeros': + if router.ssh_user: + details.append(f'user={router.ssh_user}') + if hostname: + details.append(f'hostname={hostname}') + if model and model != 'Unknown': + details.append(f'model={model}') + if version and version != 'Unknown': + details.append(f'version={version}') + if uptime and uptime != 'Unknown': + details.append(f'uptime={uptime}') + else: + if auth_mode: + details.append(f'auth={auth_mode}') + if http_status: + details.append(f'http={http_status}') + if server: + details.append(f'server={server}') + if backup_available is not None: + details.append(f'backup_available={"yes" if backup_available else "no"}') + if hostname: + details.append(f'hostname={hostname}') detail_suffix = f' ({", ".join(details)})' if details else '' if result.get('success'): diff --git a/backend/tests/test_routeros_logging_and_files_filters.py b/backend/tests/test_routeros_logging_and_files_filters.py new file mode 100644 index 0000000..f71e614 --- /dev/null +++ b/backend/tests/test_routeros_logging_and_files_filters.py @@ -0,0 +1,125 @@ +from datetime import datetime + +from app.db.session import SessionLocal +from app.models.backup import Backup + +from fastapi.testclient import TestClient + +from app.main import app + + +def _login(client: TestClient) -> dict[str, str]: + response = client.post('/api/auth/login', data={'username': 'admin', 'password': 'admin'}) + token = response.json()['access_token'] + return {'Authorization': f'Bearer {token}'} + + +def test_routeros_connection_test_creates_verbose_operation_log(monkeypatch): + from app.services import router_service as router_service_module + + monkeypatch.setattr( + router_service_module.router_service, + 'probe_connection', + lambda router, global_ssh_key=None, global_settings=None: { + 'success': True, + 'tested_at': datetime(2026, 4, 13, 10, 30, 0), + 'model': 'RB5009UG+S+', + 'uptime': '1d2h', + 'hostname': 'rb5009-core', + 'version': '7.18.2', + 'error': None, + 'transport': 'ssh', + 'server': None, + 'auth_mode': 'ssh', + 'http_status': None, + 'backup_available': None, + }, + ) + + with TestClient(app) as client: + headers = _login(client) + create_response = client.post( + '/api/routers', + json={ + 'name': 'core01', + 'device_type': 'routeros', + 'host': '10.10.10.1', + 'port': 2222, + 'ssh_user': 'backup', + 'ssh_password': 'secret', + 'ssh_key': None, + }, + headers=headers, + ) + assert create_response.status_code == 200 + device_id = create_response.json()['id'] + + response = client.get(f'/api/routers/{device_id}/test-connection', headers=headers) + assert response.status_code == 200 + + logs_response = client.get('/api/logs', headers=headers) + assert logs_response.status_code == 200 + assert any( + 'Connection test OK for RouterOS device core01' in item['message'] + and 'via ssh' in item['message'] + and 'target=10.10.10.1:2222' in item['message'] + and 'user=backup' in item['message'] + and 'hostname=rb5009-core' in item['message'] + and 'model=RB5009UG+S+' in item['message'] + and 'version=7.18.2' in item['message'] + and 'uptime=1d2h' in item['message'] + for item in logs_response.json() + ) + + +def test_files_endpoint_filters_backups_by_created_on_date(monkeypatch): + from app.services import router_service as router_service_module + + monkeypatch.setattr( + router_service_module.router_service, + 'export', + lambda router, global_ssh_key=None: f'/system identity set name={router.name}', + ) + + with TestClient(app) as client: + headers = _login(client) + create_response = client.post( + '/api/routers', + json={ + 'name': 'archive01', + 'device_type': 'routeros', + 'host': '10.10.10.2', + 'port': 22, + 'ssh_user': 'admin', + 'ssh_password': 'secret', + 'ssh_key': None, + }, + headers=headers, + ) + assert create_response.status_code == 200 + device_id = create_response.json()['id'] + + first = client.post(f'/api/backups/router/{device_id}/export', headers=headers) + second = client.post(f'/api/backups/router/{device_id}/export', headers=headers) + assert first.status_code == 200 + assert second.status_code == 200 + + with SessionLocal() as db: + first_backup = db.query(Backup).filter(Backup.id == first.json()['id']).first() + second_backup = db.query(Backup).filter(Backup.id == second.json()['id']).first() + first_backup.created_at = datetime(2026, 4, 12, 9, 15, 0) + second_backup.created_at = datetime(2026, 4, 13, 11, 45, 0) + db.add(first_backup) + db.add(second_backup) + db.commit() + + filtered = client.get(f'/api/backups?router_id={device_id}&created_on=2026-04-13', headers=headers) + assert filtered.status_code == 200 + payload = filtered.json() + assert len(payload) == 1 + assert payload[0]['created_at'].startswith('2026-04-13T11:45:00') + + previous_day = client.get(f'/api/backups?router_id={device_id}&created_on=2026-04-12', headers=headers) + assert previous_day.status_code == 200 + assert len(previous_day.json()) == 1 + assert previous_day.json()[0]['created_at'].startswith('2026-04-12T09:15:00') diff --git a/frontend/src/app/features/files/files-page.component.html b/frontend/src/app/features/files/files-page.component.html index 62942d9..96a07f1 100644 --- a/frontend/src/app/features/files/files-page.component.html +++ b/frontend/src/app/features/files/files-page.component.html @@ -32,6 +32,11 @@ + + + + + diff --git a/frontend/src/app/features/files/files-page.component.ts b/frontend/src/app/features/files/files-page.component.ts index 2ce7046..94716ec 100644 --- a/frontend/src/app/features/files/files-page.component.ts +++ b/frontend/src/app/features/files/files-page.component.ts @@ -70,6 +70,7 @@ export class FilesPageComponent implements OnInit { search = ''; backupType: 'export' | 'binary' | '' = ''; routerId: number | null = null; + createdOn = ''; sortBy = 'created_at'; order: 'asc' | 'desc' = 'desc'; diffText = ''; @@ -187,6 +188,7 @@ export class FilesPageComponent implements OnInit { if (this.search.trim()) params = params.set('search', this.search.trim()); if (this.backupType) params = params.set('backup_type', this.backupType); if (this.routerId !== null) params = params.set('router_id', String(this.routerId)); + if (this.createdOn) params = params.set('created_on', this.createdOn); this.api.http.get(`${this.api.baseUrl}/backups`, { params }).subscribe({ next: (files) => { @@ -209,6 +211,7 @@ export class FilesPageComponent implements OnInit { this.search = ''; this.backupType = ''; this.routerId = null; + this.createdOn = ''; this.sortBy = 'created_at'; this.order = 'desc'; this.load(); diff --git a/frontend/src/assets/i18n/en.json b/frontend/src/assets/i18n/en.json index e9e942c..dba1ac1 100644 --- a/frontend/src/assets/i18n/en.json +++ b/frontend/src/assets/i18n/en.json @@ -255,6 +255,8 @@ "searchPlaceholder": "Search by file or router", "typeLabel": "Type", "routerLabel": "Device", + "dateLabel": "Date", + "datePlaceholder": "Pick a date", "sortLabel": "Sort by", "orderLabel": "Order", "allTypes": "All types", diff --git a/frontend/src/assets/i18n/es.json b/frontend/src/assets/i18n/es.json index 3d7e6c5..c29aa1b 100644 --- a/frontend/src/assets/i18n/es.json +++ b/frontend/src/assets/i18n/es.json @@ -255,6 +255,8 @@ "searchPlaceholder": "Buscar por archivo o router", "typeLabel": "Tipo", "routerLabel": "Dispositivo", + "dateLabel": "Fecha", + "datePlaceholder": "Selecciona una fecha", "sortLabel": "Ordenar por", "orderLabel": "Orden", "allTypes": "Todos los tipos", diff --git a/frontend/src/assets/i18n/no.json b/frontend/src/assets/i18n/no.json index 9578d6b..54c1b63 100644 --- a/frontend/src/assets/i18n/no.json +++ b/frontend/src/assets/i18n/no.json @@ -255,6 +255,8 @@ "searchPlaceholder": "Søk etter fil eller ruter", "typeLabel": "Type", "routerLabel": "Enhet", + "dateLabel": "Dato", + "datePlaceholder": "Velg dato", "sortLabel": "Sorter etter", "orderLabel": "Rekkefølge", "allTypes": "Alle typer", diff --git a/frontend/src/assets/i18n/pl.json b/frontend/src/assets/i18n/pl.json index b8e0811..3c2915f 100644 --- a/frontend/src/assets/i18n/pl.json +++ b/frontend/src/assets/i18n/pl.json @@ -255,6 +255,8 @@ "searchPlaceholder": "Szukaj po pliku lub routerze", "typeLabel": "Typ", "routerLabel": "Urządzenie", + "dateLabel": "Data", + "datePlaceholder": "Wybierz datę", "sortLabel": "Sortowanie", "orderLabel": "Kolejność", "allTypes": "Wszystkie typy",