# Chapter 11 — Final API Contract (post Ch08–12 additions) Consolidated per Chapter 11's own instruction to update/extend the route table as new routes are introduced elsewhere, flagging additions explicitly. ## Additions beyond the original Chapter 11 table | Method | Path | Blueprint | Purpose | Response | Added in | |---|---|---|---|---|---| | GET | `/api/overview/browser-breakdown` | overview | Human-only browser/OS breakdown | JSON | Ch08 (Method A) | | GET | `/` | (root) | Redirect to `/overview` if authenticated, else `/login` | redirect | Ch12 follow-up | | GET | `/api/uploads` | uploads | List uploaded files, paginated | JSON (Grid.js) | Ch12 follow-up | | DELETE | `/api/uploads` | uploads | Bulk-delete uploaded files + derived data | JSON | Ch12 follow-up | `/login`, `/logout`, and `/healthz` were already listed in the original table (Ch02/Ch04) and are now actually implemented (Ch12) — no table change needed, just noting they're no longer placeholders. One narrowing: `/logout` is implemented as POST-only, not GET/POST, so a state-changing action isn't reachable via a plain GET (CSRF-safety; see `app/blueprints/auth/routes.py`). ## Architecture note: rollup recompute is now delete-safe (Ch12 follow-up) Deleting an uploaded file (`app/services/file_deletion.py`) removes its `log_entries`/`bot_hits`/`suspicious_events` rows explicitly (this project's SQLite connections don't have `PRAGMA foreign_keys=ON`, so the `ondelete="CASCADE"` in the migrations is documentation, not enforced behavior) and recomputes rollups for whatever dates the file touched. That recompute exposed a real bug in `app/services/aggregator.py`: three of its five writers only ever upserted-when-data-present and silently left stale rows behind when a day's data disappeared entirely — never reachable before deletion existed (rollups only ever grew). All five writers now use delete-then-insert consistently. See the module docstrings in `aggregator.py` and `file_deletion.py` for the full reasoning, including the deliberately-out-of-scope limitation around `ip_registry`/`blocklist_suggestions` not being recomputed on delete. ## Architecture note: cron is now optional (Ch12 follow-up) `POST /uploads` now triggers processing automatically in a background thread (`app/services/background.py`), and `GET /overview` opportunistically resumes any file left incomplete. `flask process-logs` and `flask cleanup` (`app/cli.py`) still exist and work identically to before for anyone who wants a cron-based fallback, but nothing in the app requires it anymore. See the module docstring in `app/services/background.py` for the explicit tradeoff this introduces relative to the original Chapter 02/03 "no persistent background workers, cron-triggered CLI only" stance. ## Full current route table | Method | Path | Blueprint | Purpose | Response | Auth | |---|---|---|---|---|---| | GET | `/overview` | overview | Overview tab | full page / HTMX partial | required | | GET | `/api/overview/kpis` | overview | KPI card values | JSON | required | | GET | `/api/overview/traffic-chart` | overview | Traffic-over-time series | JSON | required | | GET | `/api/overview/status-codes` | overview | Status-code breakdown | JSON | required | | GET | `/api/overview/top-urls` | overview | Top URLs table | JSON (Grid.js) | required | | GET | `/api/overview/top-referrers` | overview | Top referrers table | JSON (Grid.js) | required | | GET | `/api/overview/browser-breakdown` | overview | Human browser/OS breakdown | JSON | required | | GET | `/seo` | seo | SEO tab | full page / HTMX partial | required | | GET | `/api/seo/bot-summary` | seo | Bot summary cards | JSON | required | | GET | `/api/seo/crawl-chart-data` | seo | Crawl frequency series | JSON | required | | GET | `/api/seo/bot-status-codes` | seo | Status codes served to bots | JSON | required | | GET | `/api/seo/crawled-vs-visited` | seo | Bot vs. human URL comparison | JSON (Grid.js) | required | | GET | `/security` | security | Security tab | full page / HTMX partial | required | | GET | `/api/security/events` | security | Suspicious events table | JSON (Grid.js) | required | | GET | `/api/security/sensitive-paths` | security | Sensitive-path summary | JSON | required | | GET | `/api/security/ip/` | security | IP history drill-down | HTMX fragment | required | | GET | `/api/security/export-blocklist` | security | Blocklist export | text/plain download | required | | POST | `/uploads` | uploads | Upload a log file | HTMX fragment (queued state) | required | | GET | `/api/uploads//status` | uploads | Poll parse status | JSON or HTMX fragment | required | | GET | `/api/uploads` | uploads | List uploaded files | JSON (Grid.js) | required | | DELETE | `/api/uploads` | uploads | Bulk-delete uploaded files | JSON | required | | GET | `/login` | auth | Show login form | full page | public | | POST | `/login` | auth | Authenticate | redirect | public | | POST | `/logout` | auth | End session | redirect | required | | GET | `/healthz` | (root) | Liveness check | JSON | public | | GET | `/` | (root) | Auth-based redirect | redirect | public | ## Envelope compliance audit (Chapter 12) Every `/api/...` JSON endpoint above was checked against Chapter 11's `{"data": ..., "meta": {...}}` / `{"error": {"code", "message"}}` convention. All conform. `app/utils/envelope.py` was added this chapter as a shared helper for *new* endpoints going forward (used by the `unauthorized_handler` in `app/__init__.py`) — existing endpoints already matched the shape by hand and weren't rewritten, to avoid churn on working code. New this chapter: every `/api/...` path now returns a JSON `401` with `{"error": {"code": "unauthorized", ...}}` when unauthenticated, instead of Flask-Login's default redirect — consistent with the envelope even for auth failures. `/api/security/ip/` and `/api/security/export- blocklist` are the two exceptions where a *successful* response isn't JSON (HTMX fragment / text download, per the table above and Chapter 10) — their auth-failure response is still the JSON envelope for consistency. ## Auth model (Chapter 12) Every blueprint except `auth` and the root `/healthz` route requires a logged-in session (`@bp.before_request` + `flask_login.login_required` in each blueprint's `__init__.py`). This closes a gap that existed from Chapter 04 through Chapter 10: all dashboard and `/api/...` routes were reachable without authentication until Chapter 12 wired in Flask-Login, even though Chapter 01 specifies "one authenticated single-page shell."