Skip to content

Containerize for Coolify, standardize on uv, add CI - #1

Merged
theNEXlevel merged 8 commits into
mainfrom
chore/containerize-uv-and-ci
Aug 7, 2026
Merged

Containerize for Coolify, standardize on uv, add CI#1
theNEXlevel merged 8 commits into
mainfrom
chore/containerize-uv-and-ci

Conversation

@theNEXlevel

Copy link
Copy Markdown
Contributor

Prepares the project for deployment on Coolify and removes the Vercel path.

What changed

  • Fixes a broken audit in the browser. The API streams NDJSON but the front end called res.json() on the whole body, throwing Unexpected non-whitespace character after JSON. Now branches on content type, and shows real per-prompt progress instead of a simulated bar.
  • Removes the Vercel deployment path, collapsing three near-identical copies of the audit logic into one. They had already drifted: the Vercel copy called generate_report() without api_key/model, silently skipping LLM deduplication, so its reports kept duplicate findings.
  • Removes superseded duplicate modules (vision_aid/llm_client, vision_aid/programmatic, vision_aid/agentic_processing) and the dead render.yaml. None were reachable from any entry point.
  • Standardizes on uv with a committed uv.lock. The same seven dependencies were previously listed in two hand-maintained files with nothing pinned; requirements.txt is now a generated export, kept for graders and Colab.
  • Adds a Docker image and Coolify deployment (ghcr.io/c4g/va-dathttps://va-dat.c4g.dev).
  • Adds CI and publish workflows.
  • Updates README.md and CLAUDE.md to 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)

  • The GHCR package will be private on first publish; Coolify needs it public or a registry credential to pull it.
  • processing_scripts/llm_client/{runner,prompt_loader}.py and vision_aid/ingestion/pull_html.py are unreferenced but documented and attributed, so they were left alone.
  • No unit tests exist; CI is smoke and integration coverage only.

🤖 Generated with Claude Code

Justin McLellan and others added 8 commits August 6, 2026 23:35
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>
@theNEXlevel
theNEXlevel merged commit d079cf4 into main Aug 7, 2026
2 checks passed
@theNEXlevel
theNEXlevel deleted the chore/containerize-uv-and-ci branch August 7, 2026 04:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant