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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user