# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## What this is A thin, self-hosted **web admin console for Pulp 3**, a Linux patch-repository server. Pulp has a full REST API (`/pulp/api/v3/`) but no usable official web UI, so this project builds a focused operator console on top of that API. Context: it runs in an **air-gapped (closed) network** at the Bank of Korea IT Strategy Dept. cloud team. The console lets operators mirror, snapshot, verify, and deploy RPM/DEB patch repositories by clicking instead of running CLI commands. The full implementation spec is **`pulp-console-spec.md`** (Korean). `PLAN.md` tracks staged progress (checkboxes); `ref/design-ref.md` is the visual design standard; `MANUAL.md` is the Coder/Gitea/Coolify dev+deploy environment guide. Status: stages 0–4 are implemented and tested (dashboard, sync+polling, version list, gated deploy+audit). Stage 5 = containerization/packaging. Code lives under `app/` (`config.py`, `pulp_client.py`, `deps.py`, `views.py`, `audit.py`, `task_store.py`, `routes/`, `templates/`, `static/`); tests under `tests/`. ## Stack (decided — do not substitute) - **Backend / BFF:** FastAPI (Python), using `httpx` to call Pulp. - **Frontend:** HTMX + Jinja2 templates. **No build step.** Buttons trigger partial-HTML swaps; progress polling is done with `hx-trigger="every 3s"`. - **Styling:** Pico.css + a custom layer (`app/static/app.css`) applying the `ref/design-ref.md` tokens (Toss-style). Pretendard font is vendored locally. No decorative emoji in chrome — status uses colored chips/dots. ## Hard constraints (these are the point of the project) - **The browser never calls the Pulp API directly.** All traffic goes through the FastAPI BFF, which holds Pulp Basic Auth credentials and proxies requests. This protects credentials and avoids CORS exposure. - **No heavy frontend tooling** (React, Vite, npm build chains). HTML + HTMX only. - **Air-gap first:** minimize CDN dependence. Bundle CSS / HTMX JS under `static/` so the app runs with no internet access. - **View routes return HTML** (full pages or fragments), **not JSON** — HTMX consumes HTML. - **Self-signed CA support from day one:** the `httpx` client must accept a `verify=PULP_CA_FILE` option for the internal TLS CA (boknet CA family). ## Architecture ``` Browser (HTMX + Jinja) ──form/button──► FastAPI BFF ──Basic Auth──► Pulp REST API (/pulp/api/v3/) ◄──── partial HTML ──── ◄──────── JSON ──────── ``` The BFF receives Pulp JSON, renders it into Jinja partial templates, and returns HTML fragments that HTMX swaps into the page. ### Pulp object model (essential to understand the flow) `Remote (source def) → Repository (container) → RepositoryVersion (snapshot) → Publication (deployable form) → Distribution (public URL)` The URL operators point `yum/dnf` `baseurl` at is the **Distribution**. Two operations matter: 1. **Sync** (`POST .../repositories/rpm/rpm/{uuid}/sync/`): pulls from an external mirror and creates a new RepositoryVersion snapshot. Relatively safe — does not change what production servers receive. Returns an async **task**. 2. **Deploy** (create Publication → `PATCH` the Distribution's `publication`): **dangerous** — changes the version production actually fetches. Pulp objects reference each other by `pulp_href` (a path string); **use href, not name, in code.** Sync and deploy return async **tasks** — capture `task_href` and poll `GET {task_href}` (`state`: waiting/running/completed/failed) for completion. RPM paths use `rpm/rpm`; the DEB equivalent swaps that segment for `deb/apt`. ## Safety model (deploy is gated) — implemented in `routes/deploy.py` - **Verification gate** (re-checked server-side): only `repo_config` GPG-passing repos are deployable; the button is disabled otherwise. Note verification is **repo-level config**, so it indicates "verification is configured", not a per-snapshot cryptographic proof (real package checks happen at sync time). - **Confirmation modal** with a **preview** (current → target version, net package delta) and **type-to-confirm** (operator must type the repo name). - **Audit log → Postgres** (`audit.py`, table `deploy_audit`): who / when / repo / from-version → to-version (rollback trail). DB write failure does not 500 a completed deploy — it surfaces an `audit_ok=False` warning (TODO: hard-fail if audit becomes mandatory policy). - **Two-step clarity**: deploy = create publication → PATCH distribution; on failure the UI states clearly whether production was changed. - **Auth is a TODO** (`operator` is a placeholder; deploy-time password re-entry planned). ## Route surface (BFF) ``` GET / dashboard (full page) GET /repos repo list fragment POST /repos/{uuid}/sync start sync, return progress-bar fragment GET /repos/{uuid}/progress progress-bar fragment (polled every 3s) GET /repos/{uuid}/versions version list page GET /repos/{uuid}/deploy/confirm deploy confirmation modal (preview + type-to-confirm) POST /repos/{uuid}/deploy gated deploy (publication + distribution) + audit GET /healthz app liveness — always {"ok": true} (container health) GET /pulp-status Pulp connectivity badge (dashboard polls this) ``` Pulp access is centralized in `pulp_client.py` (`get`/`post`/`patch` with Basic Auth, plus `status`, `list_repos`, `get_repo`, `get_version`, `list_versions`, `sync_repo`, `get_task`, `list_distributions`, `get_publication`, `create_publication`, `update_distribution`, `wait_for_task`). HTML polling is the *page's* job; the client only single-fetches — except `wait_for_task`, used only for the compound deploy (publication→distribution) where server-side waiting is needed. In-flight syncs are tracked in `task_store.py` (in-memory; single instance only). View-model shaping (pure, httpx-free) lives in `views.py`. ## Configuration (env vars) `PULP_BASE_URL`, `PULP_USERNAME`, `PULP_PASSWORD` (secret), `PULP_VERIFY_TLS`, `PULP_CA_FILE` (internal CA pem), `DATABASE_URL` (Postgres for audit log), `PORT` (default 8000), `PULP_DEMO` (fake data for screen preview, default off). ## Build order (from the spec — build in working-screen increments) 1. Skeleton: FastAPI + Jinja2 + httpx, `pulp_client.py` stub, `/healthz` hitting Pulp status. 2. Dashboard (read-only repo list/cards). 3. Sync: `POST` sync → task_href → polling progress fragment. 4. Version list + verification badges. 5. Deploy: confirm modal → create publication → PATCH distribution → poll → audit log. 6. Styling, bundle static assets for air-gap, containerize. Each step should produce a working screen. Do not build everything at once. ## Commands ```bash .venv/Scripts/python.exe -m uvicorn app.main:app --reload # run (http://127.0.0.1:8000) .venv/Scripts/python.exe -m pytest -q # tests .venv/Scripts/python.exe -m ruff check app tests # lint .venv/Scripts/python.exe -m ruff format app tests # format ``` Tests mock the Pulp client via FastAPI `dependency_overrides` (route tests) or `respx` (client tests); `views.py` is unit-tested as pure functions. Audit DB writes are intercepted by monkeypatching `app.audit._write`. ## Deployment Now (demo): **Coolify** builds the repo `Dockerfile` (Build Pack: Dockerfile, Port 8000) and injects env vars — see `README.md` / `MANUAL.md`. `/healthz` is liveness only so Pulp being down doesn't fail the container. Final target is the **air-gapped** network: static assets are already vendored (no runtime CDN); only Python deps need offline reintroduction via a wheelhouse (`pip download` → swap the Dockerfile install step). Procedure in `README.md`. ## Conventions - Python 3.14 local venv; deps pinned in `requirements.txt` (container uses `python:3.13-slim`). Reference Pulp objects by `pulp_href`, not name. - Each stage should produce a working screen and stay green (tests + ruff) before commit.