diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d476f13 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +.git +.gitignore +.claude +.venv +venv +__pycache__ +*.pyc +*.pyo +*.log +server.log + +# Secrets and local config (injected via compose env_file) +backend/.env +.env +*.env + +# Runtime artifacts (mounted as volumes in compose) +backend/uploads/ +backend/outputs/ +backend/.llm_cache/ +out/ + +# Large local samples +samples/ +*.pdf diff --git a/.gitea/workflows/docker-release.yml b/.gitea/workflows/docker-release.yml new file mode 100644 index 0000000..e1a3f0d --- /dev/null +++ b/.gitea/workflows/docker-release.yml @@ -0,0 +1,92 @@ +name: Docker Release + +on: + push: + branches: [main] + tags: ["v*"] + +env: + REGISTRY: gitea.scoutitsystems.com + # Gitea container registry: {registry}/{owner}/{image} + IMAGE: gitea.scoutitsystems.com/woogi/conflict-checker + +jobs: + build-and-push: + runs-on: self-hosted + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Image tags + id: meta + run: | + short_sha="${GITEA_SHA:-${GITHUB_SHA}}" + short_sha="${short_sha:0:7}" + echo "short_sha=${short_sha}" >> "$GITHUB_OUTPUT" + + { + echo "tags<> "$GITHUB_OUTPUT" + env: + IMAGE: ${{ env.IMAGE }} + GITEA_SHA: ${{ gitea.sha }} + GITHUB_SHA: ${{ github.sha }} + GITEA_REF_TYPE: ${{ gitea.ref_type }} + GITHUB_REF_TYPE: ${{ github.ref_type }} + GITEA_REF_NAME: ${{ gitea.ref_name }} + GITHUB_REF_NAME: ${{ github.ref_name }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Gitea Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ gitea.actor }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + + release: + needs: build-and-push + if: gitea.ref_type == 'tag' || github.ref_type == 'tag' + runs-on: self-hosted + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Publish Gitea release + uses: https://gitea.com/actions/gitea-release-action@v1 + env: + NODE_OPTIONS: "--experimental-fetch" + with: + token: ${{ secrets.REGISTRY_TOKEN }} + tag_name: ${{ gitea.ref_name }} + name: Conflict Checker ${{ gitea.ref_name }} + body: | + ## Docker + + ```bash + docker login gitea.scoutitsystems.com + docker pull gitea.scoutitsystems.com/woogi/conflict-checker:${{ gitea.ref_name }} + ``` + + Deploy on a host (with `docker-compose.prod.yml` and `backend/.env`): + + ```bash + IMAGE_TAG=${{ gitea.ref_name }} docker compose -f docker-compose.prod.yml up -d + ``` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e87d122 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM python:3.12-slim-bookworm + +RUN apt-get update \ + && apt-get install -y --no-install-recommends poppler-utils \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY backend backend +COPY frontend frontend +COPY cli cli + +RUN mkdir -p backend/uploads backend/outputs backend/.llm_cache + +ENV PYTHONUNBUFFERED=1 +EXPOSE 8099 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8099/health')" + +CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8099"] diff --git a/README.md b/README.md index 9a9560b..54e2f1c 100644 --- a/README.md +++ b/README.md @@ -33,12 +33,111 @@ and the conflict taxonomy. ## Setup +### Docker (recommended) + +```bash +cp backend/.env.example backend/.env # set AI_API_KEY (and SMTP if you want email) +docker compose up -d --build +# -> http://localhost:8099 +``` + +Check health: `curl http://localhost:8099/health` + +Logs: `docker compose logs -f app` + +CLI inside the container (mount your PDF read-only): + +```bash +docker compose run --rm \ + -v "$(pwd)/samples/your_set.pdf:/data/set.pdf:ro" \ + app python cli/run_check.py /data/set.pdf --out /app/backend/outputs/cli-run +``` + +Persistent data lives in Docker volumes (`uploads`, `outputs`, `llm_cache`). To bind +mount host directories instead, replace the named volumes in `docker-compose.yml`. + +For hybrid mode (local vLLM on the host), set `LOCAL_BASE_URL=http://host.docker.internal:8000/v1` +in `backend/.env`. Compose already maps `host.docker.internal` to the host gateway on Linux. + +### Local Python + ```bash python3 -m venv .venv && . .venv/bin/activate pip install -r requirements.txt # needs system poppler-utils for pdf2image cp backend/.env.example backend/.env # then set AI_API_KEY to your OpenRouter key ``` +## Repository (Gitea) + +Source of truth is Scout IT's Gitea instance: + +| | | +|---|---| +| **Remote** | `https://gitea.scoutitsystems.com/woogi/Conflict_Checker.git` | +| **Web UI** | https://gitea.scoutitsystems.com/woogi/Conflict_Checker | + +Clone: + +```bash +git clone https://gitea.scoutitsystems.com/woogi/Conflict_Checker.git +cd Conflict_Checker +``` + +This repo's git config uses a dedicated credential store at +`~/.config/git/gitea-credentials` (not your global GitHub credentials). If push/pull +prompts for auth, log in via the Gitea web UI and create a personal access token, then +store it there or run one authenticated push so the helper saves it. + +## CI/CD (Gitea Actions → Container Registry) + +Develop locally, push to Gitea, and let a runner build/publish the Docker image. + +### One-time setup + +1. **Enable Actions** on the repo: Settings → Actions → Enable Repository Actions. +2. **Runner** — an `act_runner` registered against `gitea.scoutitsystems.com` + with the `self-hosted` label and access to a Docker daemon (typically + `/var/run/docker.sock` mounted into the runner). This instance uses + `sits-docker-runner` (`self-hosted`, `linux`, `docker`). +3. **Registry secret** — create a Personal Access Token on Gitea with at least + `write:package` (and `write:release` if you use version tags). Add it as a + repository secret named **`REGISTRY_TOKEN`** (Settings → Actions → Secrets). + +### What runs automatically + +| Trigger | Result | +|---------|--------| +| Push to `main` | Image pushed as `:latest` and `:sha-` | +| Push tag `v*` (e.g. `v0.1.0`) | Image tagged with the release + Gitea Release created | + +Image location: + +```text +gitea.scoutitsystems.com/woogi/conflict-checker: +``` + +Workflow file: `.gitea/workflows/docker-release.yml` + +### Local dev → deploy loop + +```bash +# 1. Develop and test locally (venv or docker compose build) +git add -A && git commit -m "your change" +git push origin main # CI builds :latest + +# 2. Cut a release when ready +git tag v0.1.0 && git push origin v0.1.0 + +# 3. On the deploy host — pull and run the published image +docker login gitea.scoutitsystems.com +IMAGE_TAG=v0.1.0 docker compose -f docker-compose.prod.yml up -d +# or: IMAGE_TAG=latest for the newest main-branch build +``` + +Copy `backend/.env` to the deploy host separately (never commit it). Use +`docker-compose.prod.yml` for production; keep `docker-compose.yml` for local +builds from source. + ## Run CLI (the fast tuning loop — also dumps `assertions.json` / `clusters.json` for inspection): @@ -51,9 +150,11 @@ python cli/run_check.py samples/your_set.pdf --out out/your_set Web UI (upload + view): ```bash -uvicorn backend.main:app --reload # open http://127.0.0.1:8000 +uvicorn backend.main:app --reload --port 8099 # open http://127.0.0.1:8099 ``` +Or use Docker: `docker compose up -d` (see **Setup** above). + ## Conflict categories `dimensional_disagreement`, `elevation_disagreement`, `location_mismatch`, diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..5cca7a8 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,28 @@ +# Production compose: pull the image built by Gitea Actions instead of building locally. +# +# docker login gitea.scoutitsystems.com +# IMAGE_TAG=latest docker compose -f docker-compose.prod.yml up -d +# +# Set IMAGE_TAG to a release tag (e.g. v0.1.0) or sha-abc1234 from CI. + +services: + app: + image: gitea.scoutitsystems.com/woogi/conflict-checker:${IMAGE_TAG:-latest} + ports: + - "${PORT:-8099}:8099" + env_file: + - backend/.env + environment: + APP_BASE_URL: ${APP_BASE_URL:-http://localhost:8099} + volumes: + - uploads:/app/backend/uploads + - outputs:/app/backend/outputs + - llm_cache:/app/backend/.llm_cache + extra_hosts: + - "host.docker.internal:host-gateway" + restart: unless-stopped + +volumes: + uploads: + outputs: + llm_cache: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3bba993 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +services: + app: + build: . + ports: + - "${PORT:-8099}:8099" + env_file: + - backend/.env + environment: + # Override in backend/.env for production (email links, etc.) + APP_BASE_URL: ${APP_BASE_URL:-http://localhost:8099} + volumes: + - uploads:/app/backend/uploads + - outputs:/app/backend/outputs + - llm_cache:/app/backend/.llm_cache + extra_hosts: + # Reach a vLLM box on the Docker host (hybrid text backend) + - "host.docker.internal:host-gateway" + restart: unless-stopped + +volumes: + uploads: + outputs: + llm_cache: