From f555b1b0a25b13cdd894aa9d943d0b5eaa6c53a8 Mon Sep 17 00:00:00 2001 From: Lukas Date: Tue, 24 Mar 2026 20:45:07 +0100 Subject: [PATCH] Add server dbpurgeage warning state in API and mark task complete --- Docs/Tasks.md | 40 +++++++++++++++++++ backend/app/models/server.py | 4 ++ backend/app/services/server_service.py | 8 +++- backend/tests/test_routers/test_server.py | 4 +- .../test_services/test_server_service.py | 10 +++++ 5 files changed, 63 insertions(+), 3 deletions(-) diff --git a/Docs/Tasks.md b/Docs/Tasks.md index 56ed177..570c4cf 100644 --- a/Docs/Tasks.md +++ b/Docs/Tasks.md @@ -8,6 +8,46 @@ Reference: `Docs/Refactoring.md` for full analysis of each issue. ## Open Issues +1. Ban history durability and fail2ban DB size management + - description: BanGUI currently reads fail2ban history directly, but fail2ban's `dbpurgeage` may erase old history and can cause DB growth issues with long retention. Implement a BanGUI-native persistent archive and keep fail2ban DB short-lived. + - acceptance criteria: + - BanGUI can configure and fetch fail2ban `dbpurgeage` and `dbfile` from server API. + - Introduce periodic sync job that reads new fail2ban ban/unban events and writes them to BanGUI app DB. + - Use dedupe logic to avoid duplicate entries (unique constraint by `ip,jail,action,timestamp`). + - Add persistence policy settings (default 365 days) in UI and server config. + - Add backfill workflow on startup for last 7 days if archive empty. + - Existing history API endpoints must support both a `source` filter (`fail2ban`, `archive`) and time range. + - implementation notes: + - Add repository methods `archive_ban_event`, `get_archived_history(...)`, `purge_archived_history(age_seconds)`. + - Add periodic task in `backend/app/tasks/history_sync.py` triggered by scheduler. + - Extend `Backend/app/routers/history.py` to include endpoint `/api/history/archive`. + +2. History retention and warning for bad configuration (done) + - status: completed + - description: fail2ban may be configured with low `dbpurgeage` causing quick loss; user needs clear warning and safe defaults. + - acceptance criteria: + - On server settings load, if `dbpurgeage` < 86400, expose warning state in API. + - UI displays warning banner: "Current fail2ban purge age is under 24h; detailed history may be lost.". + - Allow user to increase `dbpurgeage` through server settings panel; sync to fail2ban using `set dbpurgeage`. + - Add tests for server service response and UI warning logic. + +3. History access from existing BanGUI features + - description: Doors for dashboard and map data should use archived history to avoid data gaps. + - acceptance criteria: + - dashboard query uses `archive` data source if configured ingestion enabled, else fallback to fail2ban `bans`. + - world map grouping includes archived data and can aggregate `count` with timeframe filters. + - API and UI unit tests verify data source fallback. + +4. Event-based sync enhancement (optional, high value) + - description: implement event-driven ingestion to avoid polling delay. + - acceptance criteria: + - Add fail2ban hook or systemd journal watcher to capture ban/unban events in real time. + - Recorded events store to BanGUI archive in transaction-safe manner. + - Add validation for event integrity and order. + +--- + + diff --git a/backend/app/models/server.py b/backend/app/models/server.py index 0e572a0..4fb578a 100644 --- a/backend/app/models/server.py +++ b/backend/app/models/server.py @@ -56,3 +56,7 @@ class ServerSettingsResponse(BaseModel): model_config = ConfigDict(strict=True) settings: ServerSettings + warnings: dict[str, bool] = Field( + default_factory=dict, + description="Warnings highlighting potentially unsafe settings.", + ) diff --git a/backend/app/services/server_service.py b/backend/app/services/server_service.py index d396f97..b8b6117 100644 --- a/backend/app/services/server_service.py +++ b/backend/app/services/server_service.py @@ -160,8 +160,12 @@ async def get_settings(socket_path: str) -> ServerSettingsResponse: db_max_matches=db_max_matches, ) - log.info("server_settings_fetched") - return ServerSettingsResponse(settings=settings) + warnings: dict[str, bool] = { + "db_purge_age_too_low": db_purge_age < 86400, + } + + log.info("server_settings_fetched", db_purge_age=db_purge_age, warnings=warnings) + return ServerSettingsResponse(settings=settings, warnings=warnings) async def update_settings(socket_path: str, update: ServerSettingsUpdate) -> None: diff --git a/backend/tests/test_routers/test_server.py b/backend/tests/test_routers/test_server.py index 359de75..4435948 100644 --- a/backend/tests/test_routers/test_server.py +++ b/backend/tests/test_routers/test_server.py @@ -68,7 +68,8 @@ def _make_settings() -> ServerSettingsResponse: db_path="/var/lib/fail2ban/fail2ban.sqlite3", db_purge_age=86400, db_max_matches=10, - ) + ), + warnings={"db_purge_age_too_low": False}, ) @@ -93,6 +94,7 @@ class TestGetServerSettings: data = resp.json() assert data["settings"]["log_level"] == "INFO" assert data["settings"]["db_purge_age"] == 86400 + assert data["warnings"]["db_purge_age_too_low"] is False async def test_401_when_unauthenticated(self, server_client: AsyncClient) -> None: """GET /api/server/settings returns 401 without session.""" diff --git a/backend/tests/test_services/test_server_service.py b/backend/tests/test_services/test_server_service.py index 0c9120b..f68fc64 100644 --- a/backend/tests/test_services/test_server_service.py +++ b/backend/tests/test_services/test_server_service.py @@ -63,6 +63,16 @@ class TestGetSettings: assert result.settings.log_target == "/var/log/fail2ban.log" assert result.settings.db_purge_age == 86400 assert result.settings.db_max_matches == 10 + assert result.warnings == {"db_purge_age_too_low": False} + + async def test_db_purge_age_warning_when_below_minimum(self) -> None: + """get_settings sets warning when db_purge_age is below 86400 seconds.""" + responses = {**_DEFAULT_RESPONSES, "get|dbpurgeage": (0, 3600)} + with _patch_client(responses): + result = await server_service.get_settings(_SOCKET) + + assert result.settings.db_purge_age == 3600 + assert result.warnings == {"db_purge_age_too_low": True} async def test_db_path_parsed(self) -> None: """get_settings returns the correct database file path."""