Problem: Repository modules use structural typing to satisfy Protocol interfaces via
cast(). A function rename, parameter change, or signature mismatch would silently pass
mypy but fail at runtime.
Solution (Option B — minimal):
1. Aligned Protocol signatures in protocols.py with actual implementations:
- BlocklistRepository: dict[str, object] → dict[str, Any] (matches implementation)
- ImportLogRepository: dict[str, object] → ImportLogRow (typed model)
- GeoCacheRepository: dict[str, object] → GeoCacheRow; Iterable → Sequence
- HistoryArchiveRepository: dict[str, object] → dict[str, Any]
- ImportLogRepository: async compute_total_pages → sync (matches implementation)
2. Created CI validation script (backend/scripts/validate_repository_protocols.py)
that runs at build time to ensure all repository modules satisfy their Protocol
interfaces. Exit 0 if valid, 1 if any mismatch. Detects:
- Missing functions
- Parameter count mismatches
- Type annotation mismatches
- Return type mismatches
3. Updated backend/app/dependencies.py with explicit docstrings linking each
get_*_repo() provider to Backend-Development.md § 13.7.1, explaining the
module-as-Protocol pattern and that it is intentional and validated.
4. Documented the pattern in Backend-Development.md § 13.7.1:
'Repository Module Pattern — Module-as-Protocol Structural Compatibility'
explaining why the pattern works, risks (silent breakage), and how the
validation mitigates it.
5. Fixed type annotation in history_archive_repo.py:
- get_all_archived_history returns list[dict] → list[dict[str, Any]]
- Imported Any type
Benefits:
- Prevents silent breakage of repository interfaces
- Formalizes the module-as-Protocol pattern as intentional
- CI validation prevents regressions without refactoring cost
- All repository tests pass (53/53)
- mypy --strict passes on modified files
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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>
Consolidate duplicate _ok(), _to_dict(), ensure_list(), and is_not_found_error()
functions from 6 service modules into a single canonical implementation at
backend/app/utils/fail2ban_response.py.
Changes:
- Create fail2ban_response.py with canonical implementations
- Remove local duplicates from: ban_service, jail_service, config_service,
health_service, server_service, config_file_utils
- Update all imports to use shared module
- Add comprehensive docstrings and examples
- Update Architecture.md and Backend-Development.md documentation
Benefits:
- Single source of truth for response parsing logic
- Eliminates code duplication across service layer
- Improves maintainability and consistency
- Enables centralized bug fixes and improvements
Tests: All 228 service tests passing, no regressions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Cache setup_completed flag in app.state._setup_complete_cached after
first successful is_setup_complete() call; all subsequent API requests
skip the DB query entirely (one-way transition, cleared on restart).
- Add in-memory session token TTL cache (10 s) in require_auth; the second
request with the same token within the window skips session_repo.get_session.
- Call invalidate_session_cache() on logout so revoked tokens are evicted
immediately rather than waiting for TTL expiry.
- Add clear_session_cache() for test isolation.
- 5 new tests covering the cached fast-path for both optimisations.
- 460 tests pass, 83% coverage, zero ruff/mypy warnings.