Production-harden BroccoliDetect: security, reliability, frontend resilience, tests & docs
Summary
Takes the BroccoliDetect proof-of-concept from a working prototype to a
production-ready app. It hardens the internet-facing backend, makes the
frontend resilient and accessible, adds a frontend test suite, tightens the
Docker/Render setup, and rewrites the documentation. Local-dev behaviour is
unchanged by default (auth stays off unless API_KEY is set).
This is a wide change (43 commits, ~44 files) because the PoC needed work in
almost every layer. The history is atomic and each commit message stands on its
own, so it reads best commit-by-commit. main hasn't moved since this
branch was cut, so it merges as a clean fast-forward (no conflicts).
Why
The PoC ran, but it wasn't safe to expose or easy to hand over: no auth or rate
limiting, model inference blocking the event loop, print-based logging, no
tests, scattered configuration, and a thin README. This MR closes those gaps.
What changed
Security (backend + nginx)
- Optional API-key auth on
/api/detect— constant-time compare, enforced only whenAPI_KEYis set. - Per-IP sliding-window rate limiting →
429withRetry-After. - Layered upload defence: request-body cap (
413), streamed file-size cap, decompression-bomb pixel cap, real-image decode check, and a saved filename derived from the decoded format (never the user-supplied name). - Optional SHA-256 weights integrity check before
torch.load. - Hardened CORS allowlist; nginx security headers + gzip + long-cache for fingerprinted assets; non-root container;
/docshidden in production; background upload-retention sweep.
Backend reliability & correctness
- Run YOLO inference/annotation off the event loop in a threadpool, with a lock around the non-thread-safe model.
- Real
503when the model isn't loaded (no more silent failure); new/api/readyprobe;/api/healthreturns503when degraded. - Division-by-zero guard in the mm/pixel math; camera-height validation (100–5000 mm).
- Request correlation IDs (
X-Request-ID) + a global error handler.
Backend architecture & code quality
- Single source of truth in
app/config.py; repeated literals pulled into constants. - Thin routes + small single-purpose services; extracted
detection_filters; dependency injection via FastAPIDepends(testable). - Structured
loggingreplacesprint; one structured log line per completed detection. - Unified confidence default (0.40) and raised the API floor (0.10).
Frontend resilience, UX & accessibility
- All backend calls centralised in
src/api/client.js(checksresponse.ok, surfaces the request-id in errors). -
ErrorBoundary+ defensive rendering against corrupt storage / malformed payloads. - Cancellable uploads + 60 s watchdog/timeout; revoke preview blob URLs (memory-leak fix).
- Dark mode with no first-paint flash; last detection persists across reload via
sessionStorage. - Keyboard-accessible drop zone and crown rows; light-mode readability fix on the model info panel.
Testing
- New Vitest + Testing Library suite: API client, settings clamping, error boundary, defensive rendering, keyboard a11y, single-sourced constants, dark-mode FOUC prevention, and the upload abort/timeout flow.
DevOps / build / deploy
-
.env.example+ docker-compose reads.env;.dockerignorefor both build contexts. - Arch-neutral PyTorch wheels (builds on Apple Silicon and Intel); pinned frontend deps +
npm ci; weights baked into the compose image. - Backend
HEALTHCHECK, frontend gated on backend readiness, CPU/memory limits, documented single-process Uvicorn choice, andrender.yamldeploy notes.
Docs
- Full
README.mdrewrite (architecture diagrams, config table, API reference, coding principles, deploy notes) +docs/size-estimation.mddeep dive.
API / behaviour changes (for reviewers)
-
/api/detect: new optional form fieldsconf_thresholdandaspect_ratio_filter; the response now also includesconf_threshold,aspect_ratio_filter, andnum_filtered. May now return401(only whenAPI_KEYis set),413, or429. -
/api/healthnow returns503when the model isn't loaded; new/api/readyendpoint. - New environment variables (see
.env.example). Defaults keep auth disabled, so existing local-dev flows are unchanged.
How to verify
- App:
cp .env.example .env && docker compose up --build→ http://localhost:8080 - Frontend tests:
cd frontend && npm ci && npm test
Follow-ups (not in this MR)
- Backend
pytestsuite (the service layer is structured for it). - Shared store (e.g. Redis) for rate-limit/retention state if running multi-instance.
- Use real RGB-D depth instead of the fixed-height assumption.