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",