Add GET /api/dashboard/bans/trend endpoint
Implement time-bucketed ban aggregation for dashboard trend charts: - Add BanTrendBucket / BanTrendResponse Pydantic models and BUCKET_SECONDS / BUCKET_SIZE_LABEL / bucket_count helpers to ban.py - Add ban_service.ban_trend(): queries fail2ban DB with SQL bucket grouping, fills zero-count buckets, respects origin filter - Add GET /api/dashboard/bans/trend route in dashboard.py - 20 new tests (10 service, 10 router); 480 total pass, 83% coverage - ruff + mypy --strict clean
This commit is contained in:
@@ -4,7 +4,9 @@ Provides the ``GET /api/dashboard/status`` endpoint that returns the cached
|
||||
fail2ban server health snapshot. The snapshot is maintained by the
|
||||
background health-check task and refreshed every 30 seconds.
|
||||
|
||||
Also provides ``GET /api/dashboard/bans`` for the dashboard ban-list table.
|
||||
Also provides ``GET /api/dashboard/bans`` for the dashboard ban-list table,
|
||||
``GET /api/dashboard/bans/by-country`` for country aggregation, and
|
||||
``GET /api/dashboard/bans/trend`` for time-bucketed ban counts.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -20,6 +22,7 @@ from app.dependencies import AuthDep
|
||||
from app.models.ban import (
|
||||
BanOrigin,
|
||||
BansByCountryResponse,
|
||||
BanTrendResponse,
|
||||
DashboardBanListResponse,
|
||||
TimeRange,
|
||||
)
|
||||
@@ -161,3 +164,45 @@ async def get_bans_by_country(
|
||||
origin=origin,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/bans/trend",
|
||||
response_model=BanTrendResponse,
|
||||
summary="Return ban counts aggregated into time buckets",
|
||||
)
|
||||
async def get_ban_trend(
|
||||
request: Request,
|
||||
_auth: AuthDep,
|
||||
range: TimeRange = Query(default=_DEFAULT_RANGE, description="Time-range preset."),
|
||||
origin: BanOrigin | None = Query(
|
||||
default=None,
|
||||
description="Filter by ban origin: 'blocklist' or 'selfblock'. Omit for all.",
|
||||
),
|
||||
) -> BanTrendResponse:
|
||||
"""Return ban counts grouped into equal-width time buckets.
|
||||
|
||||
Each bucket represents a contiguous time interval within the selected
|
||||
window. All buckets are returned — empty buckets (zero bans) are
|
||||
included so the frontend always receives a complete, gap-free series
|
||||
suitable for rendering a continuous area or line chart.
|
||||
|
||||
Bucket sizes:
|
||||
|
||||
* ``24h`` → 1-hour buckets (24 total)
|
||||
* ``7d`` → 6-hour buckets (28 total)
|
||||
* ``30d`` → 1-day buckets (30 total)
|
||||
* ``365d`` → 7-day buckets (~53 total)
|
||||
|
||||
Args:
|
||||
request: The incoming request (used to access ``app.state``).
|
||||
_auth: Validated session dependency.
|
||||
range: Time-range preset.
|
||||
origin: Optional filter by ban origin.
|
||||
|
||||
Returns:
|
||||
:class:`~app.models.ban.BanTrendResponse` with the ordered bucket
|
||||
list and the bucket-size label.
|
||||
"""
|
||||
socket_path: str = request.app.state.settings.fail2ban_socket
|
||||
|
||||
return await ban_service.ban_trend(socket_path, range, origin=origin)
|
||||
|
||||
Reference in New Issue
Block a user