Add CLAUDE.md documenting project architecture and deployment
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
22bb14e5d9
commit
bc6ee2cf27
1 changed files with 139 additions and 0 deletions
139
CLAUDE.md
Normal file
139
CLAUDE.md
Normal file
|
|
@ -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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue