- Open an issue first for non-trivial changes so the approach can be discussed.
- Every PR must pass CI (backend tests + frontend build) before review.
- No dead code, no stale comments, no magic numbers — all tuneable values belong in
backend/config.pyor.env.
cd backend
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
uvicorn main:app --reload.env belongs at the project root (alongside docker-compose.yml), not inside backend/. The backend searches ../ automatically.
For PostgreSQL support, also install:
pip install -r backend/requirements-postgres.txtcd frontend
npm install
npm run devThe Vite dev server proxies /api/* to http://localhost:8000 — run both simultaneously.
# From repo root
python -m pytest tests/ -v# Frontend type-check and build
cd frontend && npm run buildUse imperative present tense in the subject line. Keep it under 72 characters.
<type>: <short summary>
<optional body — wrap at 72 chars>
Types: feat, fix, refactor, test, docs, chore
Examples:
feat: add Shodan source plugin
fix: handle empty stream URL in frame fetch
docs: update AI configuration table
A .gitmessage template is included in the repo root. To use it locally:
git config commit.template .gitmessage- All HTML extraction belongs in the source's own private module (e.g.
sources/insecam/_parse.py). No source-specific parsing logic inbackend/utils/. - All HTTP setup belongs in
backend/utils/http.py. - Routes must not import source modules directly — the background thread spawned by
_run_scrapeis the only caller ofscrape(). - Pydantic models are the single source of truth for data shapes. No ad-hoc dicts crossing the API boundary.
- Type annotations required on all public functions.
CountryMetaand any other source-specific model belongs inside the source package, not inbackend/models/.
- All API calls live in
src/utils/api.ts. Components never callfetchdirectly. - State that spans multiple components lives in
App.tsxor a hook. Components receive props. - No inline
styleobjects defined inside JSX — keep them in thestylesconstant at the bottom of each file. lucide-reacticons only — no other icon libraries.
Set AI_ENABLED=true and OPENAI_API_KEY=<your key> in .env. AI is opt-in and off by default. All other AI env vars have sane defaults (see .env.example).
Any OpenAI-compatible endpoint works — set OPENAI_BASE_URL to point at OpenRouter, Ollama, or any other compatible provider.
- Scene analysis and intelligence briefs are always user-triggered — nothing runs automatically.
- Bulk queue workers default to 3 concurrent jobs (
AI_QUEUE_WORKERS). Keep this low during development. - Scene analysis uses
detail: "low"for image inputs (cheaper, sufficient for surveillance frames). - Intelligence briefs are capped at 600 tokens (
_INTEL_MAX_TOKENSinai/service.py). - Re-running analysis overwrites the previous result — no history is kept.
fetch_framevalidates that a complete JPEG was received (SOI\xff\xd8and EOI\xff\xd9both present). A partial frame returnsNone, which marks the queue jobfailedrather than sending corrupted image data to the model.
- All system prompts live in
backend/ai/prompts.pyas module-level constants. Never inline a prompt in a service method or route. - Prompts use plain English imperative style. Brevity over length — the models are called for many cameras.
- When updating prompts, include a brief comment explaining the intent of any non-obvious instruction.
- Every scene analysis job goes through
analysis_queue— single camera or bulk, same code path. analysis_queue.add(ids)is idempotent for pending/processing cameras. Re-adding a done/failed camera re-queues it (retry semantics).- Workers auto-start when cameras are added via
addCamerasToQueueinuseAnalysisQueue.ts. ThePOST /ai/queue/startendpoint is idempotent — safe to call when workers are already running. - Workers use
asyncio.run()to call async AI methods — each worker thread owns its own event loop. ai.serviceandapi.storeare imported lazily inside_worker_loopto avoid circular imports.- State transitions:
pending → processing → done | failed. Every transition emits aQueueProgressEventover SSE.
See the "Adding a new AI feature" section in AGENTS.md for the step-by-step checklist.
- Never remove the
scraper_page_delaybetween country batches. - Do not increase default
scraper_workersabove 20 in PRs — respect the source's infrastructure. - The scraper must remain read-only: no writes, no authentication attempts, no form submissions.
- CI passes (backend tests + frontend build)
- No new
TODOorFIXMEcomments without a linked issue - Docstrings updated on any modified public functions
- If the
Camera,ScrapeProgress, orSourceMetamodel changed: bothbackend/models/andfrontend/src/types/index.tsare updated in the same PR