From bc6ee2cf279a4714381bab4f22f53b8497328aab Mon Sep 17 00:00:00 2001 From: Barely Removable Date: Tue, 25 Aug 2026 16:40:24 -0700 Subject: [PATCH] Add CLAUDE.md documenting project architecture and deployment Co-Authored-By: Claude Sonnet 5 --- CLAUDE.md | 139 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..32fe622 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,139 @@ +# Card Grader + +Self-hosted web app that estimates PSA trading card grades from photos, using +vision-capable LLMs (Claude and/or GPT) as the grading engine. Runs as a +single Docker container on the user's Unraid home server, exposed at +`hippofam.com/cards` through Nginx Proxy Manager. + +No framework: the whole backend is Python stdlib +(`http.server.ThreadingHTTPServer`) plus `sqlite3`. Optional third-party +packages are `anthropic`, `openai`, and `Pillow` (image processing only — +never required for the server to boot). + +## Architecture + +- **`app.py`** — HTTP routing/handlers. Reads Basic Auth username forwarded + by the reverse proxy for *attribution only* (who graded what), never for + access control — auth itself is enforced by Nginx Proxy Manager in front + of the container via htpasswd. Admin-only settings are gated by + `ADMIN_USERNAME` (`CARD_GRADER_ADMIN_USER` env var). Request bodies are + capped (`MAX_BODY_BYTES`); oversized/malformed bodies get a 413 and the + connection is closed rather than trusting a client-supplied length. +- **`vision.py`** — Provider abstraction over Anthropic and OpenAI vision + APIs (`_call_anthropic` / `_call_openai`, dispatched from a common + entrypoint). `MODELS` holds per-model pricing/capabilities. + `GRADING_SYSTEM` is the large system prompt encoding PSA's actual grading + rubric (centering tolerances, corner/edge/surface criteria, qualifiers — + MC/OC/PD/ST/MK, half-grades, Altered Authentic) — verified against + psacard.com, beckett.com, and cgccards.com, not guessed. Self-reported + "high" confidence is capped to "medium" when neither centering nor edge + whitening was independently measurable from the image geometry. +- **`cardimage.py`** — Pillow-based image pipeline: card boundary detection, + corner/edge/surface crop generation, centering/edge-whitening/aspect-ratio + measurement from pixels (not just left to the model's eye). Includes + holder/toploader-aware detection (`detect_card_box`): a combinatorial + search over candidate edge positions constrained to standard card aspect + ratios, so the app can locate the actual card inside a slab/toploader + rather than just detecting the holder's outline. When a holder is + detected, edge-whitening measurement is refused (the holder's own inner + edge corrupts that specific measurement) but centering still runs + (border-width geometry, unaffected by clear plastic), and aspect-ratio + measurement deliberately uses the *unrefined* box to avoid circularity + (refinement selects for standard-ratio rectangles, so measuring the + refined box's ratio would trivially always look "normal"). +- **`store.py`** — SQLite (WAL mode) persistence. `grades` and + `grade_events` tables, JSON columns for structured data, schema + migrations via idempotent `ALTER TABLE` in `store.init()`. List/detail + API responses explicitly exclude `source_images_json` to keep payloads + small. DB path overridable via `CARD_GRADER_DB_PATH`. +- **`static/`** — Vanilla JS/CSS/HTML frontend, PWA-enabled (manifest, + service worker). "PSA slab" visual theme. `__BASE__` is templated at + request time so the app works correctly when served from a sub-path + (`/cards`) behind the proxy. + +## Local development + +```bash +python3 app.py +``` + +Needs `ANTHROPIC_API_KEY` and/or `OPENAI_API_KEY` in the environment to +actually grade; the server itself has no required third-party deps. A local +`grades.db` is gitignored and is scratch/test data only — it is **not** the +production database (that lives on the server, see below). + +## Deployment + +**Target:** Unraid server, container path `/mnt/user/appdata/card-grader`, +built from `Dockerfile` / `docker-compose.yml` in this repo. Non-root +container user (`99:100` / `nobody:users`), healthcheck, log rotation, +`init: true`. Reverse-proxied by Nginx Proxy Manager at `hippofam.com/cards` +with per-user HTTP Basic Auth. + +**Current deploy mechanism (as of 2026-08-25): manual.** Changes are copied +to the server by hand (`scp`) and the container is rebuilt over SSH; the two +copies of the repo (local Mac, server) are kept in sync by committing on +both sides. There is currently no automated push-to-deploy path connected — +see below. + +**Pending: restricted git-push deploy.** A setup script +(`setup-card-grader-deploy.sh`, delivered to the user, not yet run) creates +a scoped `carddeploy` user on the Unraid box for exactly this purpose: + +- Login shell is `git-shell` — accepts git push/pull only, nothing else (no + interactive shell, no arbitrary commands, no SFTP). +- A `post-receive` hook (root-owned, mode 755 — not writable by the + `carddeploy` account, and not something `git push` can overwrite via the + protocol regardless) triggers a single root-owned deploy script via one + narrowly-scoped `sudo` rule (`NOPASSWD` for that *exact script path* + only — deliberately never raw `docker`/the `docker` group, both of which + are root-equivalent on the whole box via the daemon socket). +- The deploy script does `git checkout -f main` into the live app directory + and `docker compose up -d --build`. +- State (the bare repo, the deploy script, the sudoers source file) lives + under `/mnt/user/appdata/card-grader-deploy/` on the array — Unraid boots + from USB into RAM, so anything living only under `/` would vanish on + reboot. Setup is reinstalled idempotently via `/boot/config/go` (Unraid's + official boot-time persistence hook) so it survives reboots. + +Once the user runs that script and confirms, the local SSH config alias +`unraid-cardgrader` (in `~/.ssh/config`, already pointed at the new +`carddeploy` user and a fresh dedicated keypair +`~/.ssh/id_ed25519_cardgrader_deploy`) will be usable, and the remaining +step is adding the bare repo as a git remote here and doing a first push to +verify the pipeline end-to-end. **Do not assume this pipeline is live** — +confirm with the user before relying on it. + +A prior root SSH key/access to the whole box was explicitly revoked by the +user; the replacement above is intentionally scoped to *only* updating this +one container, not general Unraid/Docker access. + +## Conventions and constraints established for this project + +- PSA grading rules must be verified against a real source (PSA's own site, + Beckett, CGC) before being encoded into `GRADING_SYSTEM` — this rubric has + had real accuracy bugs (e.g. a self-contradictory centering tolerance, a + missing 5% front-centering leeway rule for grades 7+) found and fixed + through actual verification, not assumption. +- The vision prompt's `cannot_assess` escape hatch is deliberately narrow: + surface condition genuinely can't be separated from dust/scratches on a + toploader/slab's plastic, so it keeps the escape hatch. Corner and edge + geometry (sharpness, whitening, chipping) **does** read through clear + plastic and must be judged normally — an earlier prompt version gave the + model an escape hatch for corners/edges too, which caused false + "cannot tell from photo" results on cards that were plainly visible + through the holder. Don't reintroduce that. +- No pre-flight/"precheck" step before a paid grading call. This was tried + and explicitly rejected by the user — their workflow is screenshots that + can't be pulled out of the toploader, so a pre-check step doesn't fit and + was reverted. +- No browser automation (Playwright or otherwise) to scrape eBay listings. + Tried a plain `curl`-based fetch once (not Playwright) and got an + immediate 403; iterating on headers/fingerprinting to get past that would + be bot-detection evasion regardless of which tool performs it, and is + out of scope. This feature is paused; if revisited, the legitimate path + is a client-side bookmarklet/extension that uses the *user's own* + authenticated browser session rather than a server-side fetch. +- Never trust the reverse proxy's forwarded auth header for anything beyond + attribution (whose name to log against a grade) — access control is the + proxy's job, not the app's.