Fix: Consolidate divergent _since_unix implementations (T-09)

Consolidate the two divergent implementations of _since_unix from ban_service.py
and history_service.py into a single shared utility function in time_utils.py.

Changes:
- Move _since_unix to app/utils/time_utils.py with consistent time.time() approach
- Move TIME_RANGE_SLACK_SECONDS constant to app/utils/constants.py
- Update ban_service.py to import since_unix from time_utils
- Update history_service.py to import since_unix from time_utils
- Both services now use the same window boundary calculation with 60-second slack
- Add comprehensive tests for the shared since_unix function
- Document timestamp handling rationale in Backend-Development.md

This ensures dashboard and history queries return consistent row counts for the
same time range by using the same timestamp calculation and slack window across
all services.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-25 18:44:59 +02:00
parent 420ea18fd9
commit ac2028e1c2
6 changed files with 132 additions and 51 deletions

View File

@@ -65,6 +65,9 @@ TIME_RANGE_HOURS: Final[dict[str, int]] = {
TIME_RANGE_365D: 365 * 24,
}
TIME_RANGE_SLACK_SECONDS: Final[int] = 60
"""Clock drift and test seeding tolerance for timestamp comparisons."""
# ---------------------------------------------------------------------------
# Pagination
# ---------------------------------------------------------------------------

View File

@@ -7,6 +7,7 @@ for working with time throughout the backend.
"""
import datetime
import time
def utc_now() -> datetime.datetime:
@@ -65,3 +66,31 @@ def hours_ago(hours: int) -> datetime.datetime:
Timezone-aware UTC :class:`datetime.datetime`.
"""
return utc_now() - datetime.timedelta(hours=hours)
def since_unix(range_: str) -> int:
"""Return the Unix timestamp for the start of a time-range window.
Uses :func:`time.time` (always UTC epoch seconds on all platforms) to be
consistent with how fail2ban stores ``timeofban`` values in its SQLite
database. fail2ban records :func:`time.time()` values directly, so using
a timezone-aware :func:`datetime.datetime.now`\\ ``(UTC).timestamp()``
would theoretically produce the same result but using :func:`time.time`
avoids any timezone-aware datetime pitfalls on misconfigured systems.
A 60-second slack window is applied to accommodate clock drift and
test seeding delays. This ensures consistent query windows across services
(e.g., dashboard vs. history).
Args:
range_: One of the supported time-range presets (e.g., ``"24h"``).
Returns:
Unix timestamp (seconds since epoch) representing the start of the
time window: *now range_ slack*.
"""
from app.models.ban import TIME_RANGE_SECONDS # noqa: F401
from app.utils.constants import TIME_RANGE_SLACK_SECONDS
seconds: int = TIME_RANGE_SECONDS[range_]
return int(time.time()) - seconds - TIME_RANGE_SLACK_SECONDS