feat: implement API versioning /api/v1/

- All backend routers moved to /api/v1/ prefix
- Frontend BASE_URL updated to /api/v1
- Setup redirect middleware updated to redirect to /api/v1/setup
- Health router path fixed: prefix=/api/v1/health, @router.get('')
- conftest.py: set server_status=online for test fixture
- Created Docs/API_VERSIONING.md with deprecation policy
- Updated Docs/Backend-Development.md with versioning section
- Updated Instructions.md curl examples

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-05-02 21:29:30 +02:00
parent 0d5882b32f
commit cc6dbcf3f0
51 changed files with 1886 additions and 671 deletions

View File

@@ -260,6 +260,50 @@ For `history_archive`, the read-heavy workload justifies these indexes because:
---
## 7.6 Never Load Unbounded Result Sets
**Problem:** Loading large result sets entirely into Python memory causes:
- Memory spikes that crash containers
- Slow dashboard performance
- Unbounded database file growth
**Rule:** Never load unbounded result sets. Always use SQL aggregation or pagination.
**Anti-patterns:**
```python
# BAD — loads all rows into memory
all_rows = await history_archive_repo.get_all_archived_history(db=db, ...)
# GOOD — SQL aggregation returns lightweight counts
ip_counts = await history_archive_repo.get_ip_ban_counts(db=db, ...)
```
**SQL aggregation patterns for common operations:**
| Operation | SQL Pattern | Repository Function |
|-----------|-------------|---------------------|
| Count by IP | `SELECT ip, COUNT(*) FROM bans GROUP BY ip` | `get_ip_ban_counts()` |
| Count by jail | `SELECT jail, COUNT(*) FROM bans GROUP BY jail` | `get_jail_ban_counts()` |
| Count by time bucket | `SELECT CAST((timeofban - ?) / ? AS INTEGER), COUNT(*) ... GROUP BY bucket_idx` | `get_ban_counts_by_bucket()` |
| Paginated rows | `WHERE id < ? ORDER BY id DESC LIMIT ?` | `get_archived_history_keyset()` |
**When to use SQL aggregation:**
- Computing totals, counts, or aggregations for display
- Building country/jail/geo maps from large datasets
- Any endpoint that needs only a summary, not full row data
**When to use pagination:**
- Endpoints that return individual records for display (ban lists, history)
- Any endpoint where clients need access to specific rows
**Memory budgets for reference:**
- 1M ban records ≈ 200-400 MB if fully materialized as Python dicts
- SQL aggregation returns lightweight results: {ip, count} pairs = a few KB for same 1M records
- Keyset pagination returns only the page size (typically 50-200 rows)
---
## 3. Project Structure
```
@@ -1840,12 +1884,14 @@ async def client() -> AsyncClient:
@pytest.mark.asyncio
async def test_list_jails_returns_200(client: AsyncClient) -> None:
response = await client.get("/api/jails/")
response = await client.get("/api/v1/jails/")
assert response.status_code == 200
data: dict = response.json()
assert "jails" in data
```
See [API_VERSIONING.md](API_VERSIONING.md) for the full versioning strategy, deprecation policy, and instructions for adding versioned endpoints.
---
## 9.1 Background Tasks and Scheduler Architecture