Containerize for Coolify, standardize on uv, add CI - #1
Merged
Conversation
The audit endpoints stream NDJSON — progress events one JSON object per line, then a final result object — but the front end called res.json() on the whole body, which threw "Unexpected non-whitespace character after JSON" as soon as it hit line 2. Branch on content type: stream and dispatch NDJSON progress events, and keep res.json() as a fallback for a buffered single-object response. This also replaces the simulated progress bar with real per-prompt progress. Also drops Vercel-specific copy, including a user-facing error that told people to check a deploy dashboard they cannot see. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deployment moves to a Docker image on Coolify, so the serverless handler, its routing config, and the deploy workflow are no longer used. This also collapses three near-identical copies of the audit logic into one. The copies had already drifted: api/index.py called generate_report() without api_key/model, silently skipping the LLM deduplication pass that api_server.py performs, so reports served from Vercel kept duplicate findings. Removed: api/index.py serverless FastAPI handler entry_points/vercel_handler.py a third copy of the same logic vercel.json routing rewrites .github/workflows/production.yml deployed to Vercel on push to main Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
None of these are reachable from any entry point, and each is a stale
parallel copy of code that is still live elsewhere:
vision_aid/llm_client/ last touched 2026-03-10; the live copy in
processing_scripts/ gained Gemini support
on 2026-07-27. prompt_loader.py and
runner.py were byte-identical.
vision_aid/programmatic/ a class-based rewrite of the same checks,
abandoned 2026-03-07; run_pipeline.py
imports processing_scripts/programmatic/.
vision_aid/agentic_processing/ an empty placeholder package.
render.yaml a Render config whose 'poetry install'
build command never matched this project.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The project carried the same seven dependencies in two hand-maintained lists (pyproject.toml and requirements.txt) with no lock file, so nothing was pinned: every teammate and every Docker build resolved independently. pyproject.toml is now the only hand-edited list, uv.lock pins all 36 packages including transitives, and requirements.txt becomes a generated export kept for graders and Colab — its header carries the regeneration command, and CI fails if it drifts. .python-version pins 3.12 to match the container. uv also manages the interpreter, which avoids the 'python -m venv' ensurepip failure on Debian/Ubuntu systems without python3-venv. Also drops fastapi, uvicorn and python-multipart: they existed only for the removed serverless handler, and api_server.py uses http.server. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Multi-stage build: uv sync --frozen into a builder, then the venv copied
into a clean runtime with no uv or build tooling. --frozen fails the build
if uv.lock and pyproject.toml disagree.
Ordering matters here: creating the user before the venv and copying with
--chown avoids a chown -R over /app/.venv, which duplicates the whole
virtualenv into a second layer and doubles the image.
docker-compose.yml is for local runs; Coolify deploys the published image
directly, so its read_only/tmpfs hardening does not apply in production.
It deliberately passes no provider API keys — users supply their own in
the web form, and an interpolated ${ANTHROPIC_API_KEY:-} would silently
inject a developer's local .env key into the container and bill it for
every anonymous audit.
DEPLOY.md covers the proxy settings that matter: response buffering breaks
the progress stream, and short read timeouts cut off long audits.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ci.yml runs on pull requests to main and needs no API key — every check is free. It verifies uv.lock matches pyproject.toml, requirements.txt matches the lock, entry points import, a full pipeline dry run produces prompts and findings while consuming zero tokens, index.html's inline JavaScript parses, and the image builds, becomes healthy, serves the site and returns a valid NDJSON audit. publish.yml runs on push to main: builds the image, pushes it to ghcr.io/c4g/va-dat, smoke tests the published image, then triggers the Coolify deploy. The smoke test exists because CI only runs on pull requests, so the merge commit itself is otherwise never exercised before it deploys; it also asserts the image carries no credentials. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both had drifted far enough to mislead. CLAUDE.md described a venv/pip
setup, told contributors not to add JavaScript to a site that is now ~700
lines of inline JS, and made no mention of api_server.py, Docker, Coolify
or uv.
Documents what exists now: the uv workflow, the web app and its NDJSON
contract, the deployment path, and CI. Records two failure modes that cost
real money during this work — a 'dry run' still spends if a key resolves
from .env (sending api_key: "" does not force one), and compose
interpolation of ${ANTHROPIC_API_KEY:-} reads the host's .env.
README gains Running the Web App, Deployment and CI sections, and its
structure tree now matches the repository.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
.claude/worktrees/objective-spence was committed as a mode 160000 gitlink with no matching .gitmodules entry, so git records a submodule it can never fetch. Every checkout — including each CI run — fails its submodule cleanup with 'fatal: No url found for submodule path' and exit code 128. Removes it from the index only; the directory on disk is untouched, and .claude/worktrees/ is now ignored. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Prepares the project for deployment on Coolify and removes the Vercel path.
What changed
res.json()on the whole body, throwingUnexpected non-whitespace character after JSON. Now branches on content type, and shows real per-prompt progress instead of a simulated bar.generate_report()withoutapi_key/model, silently skipping LLM deduplication, so its reports kept duplicate findings.vision_aid/llm_client,vision_aid/programmatic,vision_aid/agentic_processing) and the deadrender.yaml. None were reachable from any entry point.uv.lock. The same seven dependencies were previously listed in two hand-maintained files with nothing pinned;requirements.txtis now a generated export, kept for graders and Colab.ghcr.io/c4g/va-dat→ https://va-dat.c4g.dev).README.mdandCLAUDE.mdto match what actually exists.Verification
Every CI step was executed locally before being committed. The image builds at 301 MB, runs read-only as a non-root user, becomes healthy, and returns a valid NDJSON audit.
CI needs no API key — a request with no key resolvable runs in dry-run mode, and the workflow asserts zero tokens were consumed.
Follow-ups (not in this PR)
processing_scripts/llm_client/{runner,prompt_loader}.pyandvision_aid/ingestion/pull_html.pyare unreferenced but documented and attributed, so they were left alone.🤖 Generated with Claude Code