You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(api): wire suppression labels, fix commit ordering, harden proxy and CORS (#64)
Closes the remaining correctness findings from the backend audit.
- suppression_labels was stored, exposed via the API and editable in the UI,
but handle_pull_request_opened never read it: configuring labels had zero
effect. Now read (case-insensitively) before any session is created.
- The PR comment was posted while the session row was still uncommitted, so a
commit failure left a public link to a session that never existed. It also
held a pool connection across ~40s of GitHub I/O. The session is committed
before anything is announced.
- uvicorn trusted X-Forwarded-* from 127.0.0.1 only, so behind Traefik every
client shared the proxy IP and all rate limits collapsed into one global
bucket — a single client could lock everyone out of login.
- The pool was sized 20+10 per worker against a stock PostgreSQL: 4 workers
could ask for 120 of 100 connections. Now configurable, defaulting to 10+5.
- Unhandled exceptions returned a 500 without CORS headers (the handler runs
outside the middleware stack), so real errors reached the browser as opaque
CORS failures.
- get_current_user accepted ?access_token= on every route, leaking JWTs into
proxy logs and browser history. Split into CurrentUser (header only) and
StreamUser (header or query), the latter used by the SSE endpoint alone.
363 tests pass, coverage 84%, ruff and mypy clean.
-**Layered modules**: each domain module is `router.py` (thin — validate, call one use case, shape the response) → `service.py` (use cases, no SQL, no HTTP) → `repository.py` (every query, including the soft-delete predicate) → boundary modules for external systems (`github.py`, `anthropic.py`, `docker_client.py`), all returning typed objects rather than dicts. `container` additionally splits `streaming.py` (SSE pipeline) and `cleanup.py` (reaping) out of the service.
49
+
-**Container orchestration**: `container` module manages ephemeral Docker lifecycle, credential injection, result relay. `finalize_session()` (mark completed → scorecard → PR comment) is deliberately detached from the HTTP request, so a client disconnect cannot leave a session stuck RUNNING.
50
50
-**Skills as agents**: each skill is a self-contained folder with workflow definitions, mounted into containers
51
51
-**SSE passthrough**: backend relays container output to frontend (no AI response generation in backend)
52
52
-**Conversation UI**: frontend renders session output as a structured conversation with markdown (react-markdown + remark-gfm), syntax highlighting (shiki with JS regex engine), and diff coloring. Components: `ConversationOutput` (scroll container) -> `MessageBlock` (role dispatch) -> `MarkdownContent` / `CodeBlock`. Data flows as `StreamMessage[]` (structured content blocks) instead of flat text lines.
@@ -57,7 +57,7 @@ infra/
57
57
-**API prefix**: all routes under `/api/v1`
58
58
-**Admin panel**: SQLAdmin at `/admin`, configured in `admin/views.py`
59
59
-**Dashboard**: user-facing installation management at `/installations` -- installation list, session history, session replay. Authenticated users redirect from `/` to `/installations`. SQLAdmin remains at `/admin` as superadmin escape hatch.
60
-
-**Cross-module queries**: installation module queries `ContainerSession` model directly (inline import in service functions) for session counts and lists. This avoids circular imports while keeping the API surface on the installation router.
60
+
-**Cross-module queries**: a module never writes SQL over another module's tables. `container/repository.py` owns every `ContainerSession` query, including the aggregates the identity dashboard and the installation router consume.
61
61
-**Auth on all REST routes**: identity and installation routers use `Depends(get_current_user)`, container router uses it too. The webhook handler bypasses REST routes entirely — it calls `create_session()` directly (DB record only, no container start). Container start happens when the authenticated frontend calls the REST endpoint.
62
62
-**Production env validation**: `Settings` has a `model_validator` that enforces non-empty secrets when `ENVIRONMENT=production`. Tests use `ENVIRONMENT=test` to skip this.
63
63
-**Graceful lifecycle**: lifespan reconciles stale RUNNING/PENDING sessions on boot (marks FAILED), and stops all running containers on shutdown. Periodic cleanup uses configurable `CONTAINER_TTL_SECONDS` from settings.
-**Worktrees and node_modules**: `npm install` must be run in each worktree separately — `node_modules` aren't shared from the main tree
122
122
-**Shallow clone + `gh pr checkout --detach`**: `gh pr checkout` (without `--detach`) fails on `--depth=1` clones because git can't set up tracking branches from shallow refs. Always use `--detach` — containers don't need tracking branches, just files on disk.
123
123
-**Installation IDs in URLs**: frontend routes (`/installations/:id`) use `github_installation_id` (integer, e.g. `123093268`), NOT the internal UUID. All API endpoints (installation AND container session creation) expect the GitHub integer ID. The service layer resolves to internal UUID via `get_installation_by_github_id()`.
124
-
-**Fast-failing container race**: if a container exits before the SSE stream fully drains, the client may disconnect before `mark_completed()` runs, leaving the session stuck as RUNNING with 0 persisted events. The 5-minute cleanup task marks these as TIMEOUT. Root cause: generator cancellation on client disconnect skips the post-stream `mark_completed` call in `_event_stream()`.
125
-
-**Flaky dispatcher tests**: `test_issues_opened_is_ignored_and_logged` and `test_pull_request_closed_is_ignored` fail when run as part of the full suite due to structlog `configure_logging()` state contamination from `create_app()` in earlier tests. They pass in isolation.
126
124
-**Multi-worker background tasks**: with `--workers N`, each uvicorn worker runs its own lifespan (webhook reaper + container cleanup). Both are idempotent: reaper uses atomic row-level claim (`mark_processing`), cleanup suppresses double-stop exceptions.
127
125
-**Run migrations after checkout**: `docker exec helprs-api-1 uv run alembic current` vs `alembic heads` — if they differ, run `make migrate`. Missing columns cause 500s that surface as browser CORS errors (response lacks CORS headers on unhandled exceptions).
128
-
-**API rebuild invalidates tokens**: `docker compose up --build api` may regenerate SECRET_KEY, invalidating all JWTs and refresh cookies. Re-authenticate after API restarts.
126
+
-**API rebuild invalidates tokens**: re-authenticate after an API restart if the deploy changed `SECRET_KEY`. Nothing in the code generates one — `Settings.SECRET_KEY` is required and the app refuses to boot without it.
129
127
-**Coolify `--project-directory`**: Coolify sets `--project-directory` to the repo root, not the compose file location. Relative paths in the compose (`context`, `volumes`) must be relative to the repo root (`./apps/api`, not `../../apps/api`).
130
128
-**Coolify domain persistence**: Domains set in the Coolify UI may be cleared on redeploy/reload. Verify after each deploy. If persistent issues, add Traefik labels directly in the compose.
131
129
-**`.dockerignore` vs `pyproject.toml`**: `apps/api/.dockerignore` excludes `*.md` but `pyproject.toml` references `readme = "README.md"` — `!README.md` exception is required in `.dockerignore` or `uv sync` fails.
0 commit comments