Initial commit

This commit is contained in:
Ovidiu U
2026-05-04 09:45:17 +01:00
commit 18b7e11657
31 changed files with 2838 additions and 0 deletions

97
CLAUDE.md Normal file
View File

@@ -0,0 +1,97 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project status
The repo currently contains only `wordsearch-specs.md` — the implementation has not been written yet. The spec is the source of truth; read it before any change. When implementing, follow the directory layout, routes, and behaviours it prescribes rather than improvising.
## Local development
Iterate on the Mac with a venv + `uvicorn --reload`; only use Docker when verifying the deploy. The Docker bind-mount (`./themes`) points at the same directory, so local and container runs share theme state.
```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --reload --port 8000
```
`--reload` restarts on `.py` changes; Jinja2 templates reload without a restart. `.venv/` must be in `.gitignore`.
### Dependency management
**Use `requirements.txt` with pinned versions. Do not introduce Poetry, uv, Pipenv, or `pyproject.toml`.** This is a homelab app — plain `pip` works identically in the venv and inside the Docker container with no extra tooling.
Initial pin set (don't drop any of these without checking why they're here):
```
fastapi==0.115.0
uvicorn[standard]==0.32.0
jinja2==3.1.4
reportlab==4.2.5
python-multipart==0.0.12
```
`python-multipart` is required for the form-encoded `POST /generate` and theme-editor submits — easy to omit until the form 500s.
### What not to add
No test framework, no linter, no formatter, no build step is specified. Don't introduce one without asking. The project is deliberately minimal: single language, single process, no build pipeline, no DB.
### Docker
Production runs the same code in a container — `docker compose up` mounts `./themes` as a volume. The container has no venv (the container *is* the isolation); the Dockerfile `pip install`s into the system Python. Don't try to share a venv between host and container.
## Architecture
### Two-form word model (central design idea)
Every input word produces two strings, and they are not interchangeable:
- **Display form** — original string, untouched. Goes on the PDF word list.
- **Grid form** — stripped of leading prefix tokens, uppercased, whitespace/punctuation removed. Goes in the grid and is what length filters and placement operate on.
Prefix stripping (`Mr`, `Mrs`, `Miss`, `Dr`, `Sir`, `Captain`, `Saint`, etc. — full list in `app/normaliser.py::PREFIXES`) is **token-based, case-insensitive, with optional trailing dot**. `"Misty"` does NOT match `"Miss"`. Multiple consecutive prefixes strip (`"Mr Dr Strange"``STRANGE`). A word that is *only* a prefix (`"Mr"`) keeps as `MR` and logs a stderr warning. Empty grid form after stripping → skip and warn.
The PDF word list shows `GRIDFORM (original prefix tokens)` only when something was stripped, e.g. `TICKLE (Mr)`. Bare `GRIDFORM` otherwise.
### Direction set is computed, not picked
Don't expose every direction as an independent toggle. Horizontal (→) and vertical (↓) are always on. The two user toggles compose:
```
base = { →, ↓ }
if diagonal: base |= { ↘, ↗ }
if reversed: base |= { reverse(d) for d in base }
```
So `reversed` without `diagonal` gives `→ ↓ ← ↑` (no diagonals reversed because none were in `base`). `diagonal + reversed` gives all 8.
### Placement: retry-then-skip, not backtrack
Per word: random direction + random start, validate collision (empty cells, or matching letters when overlap is on), commit or retry up to 200 times. Skip after 200 and warn — never block, never backtrack already-placed words. If fewer words placed than requested, generate the puzzle anyway and surface a warning to the result page.
### No sticky state
All optional toggles (diagonal, reversed, overlap) default **off** on every `/` page load. Don't add cookies, localStorage, or session state to remember the last config. Each puzzle is configured explicitly.
### Storage
Flat JSON files under `themes/<slug>.json` with `{ name, words: [...] }`. No DB. The slug is the filename and is locked after creation. `themes/` is the only persistence — generated PDFs are streamed straight to the browser as `<slug>_<YYYY-MM-DD_HH-MM-SS>.pdf` and never written to disk on the server.
### Server-rendered, almost no JS
Jinja2 templates only. The single piece of vanilla JS lives in the theme editor: a debounced `fetch` to `POST /api/normalise` to render the live preview of grid forms. No bundler, no framework, no build artifacts in the repo.
### Auth
None by design (homelab use). If exposing publicly, add HTTP basic auth as FastAPI middleware — don't introduce a user model.
## Conventions worth preserving
- Validation errors (invalid slug, empty word list, `max_length < min_length`) must render a clear message, never a stack trace.
- `max_length` is clamped to `grid_size` before filtering.
- All grid letters are uppercase AZ, including the random fill.
- The PDF is plain: no footer, no timestamp, no branding, no toggle state printed. The filename carries the timestamp.
- v1 ships puzzle-only — no answer key PDF.