docs: improve CLAUDE.md with architecture details, patterns, and fixes

Fix Alembic commands (add -c migrations/alembic.ini flag), add Fabric.js
annotation editor patterns, Alpine.js/Jinja2 escaping rules, template
structure, test infrastructure details, and Docker configuration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Adriano
2026-02-16 21:25:16 +01:00
parent e4c2cc3ed4
commit 9502388064
+88 -18
View File
@@ -31,11 +31,13 @@ cd client && npx tailwindcss -i static/css/input.css -o static/css/tailwind.css
### Database & Migrations
```bash
cd server && alembic upgrade head # Applica migrazioni
cd server && alembic revision --autogenerate -m "descrizione" # Genera migrazione
cd server && alembic downgrade -1 # Rollback ultima
docker compose exec server alembic upgrade head # Via Docker
# alembic.ini è in server/migrations/, serve il flag -c
cd server && alembic -c migrations/alembic.ini upgrade head # Applica migrazioni
cd server && alembic -c migrations/alembic.ini revision --autogenerate -m "descrizione" # Genera migrazione
cd server && alembic -c migrations/alembic.ini downgrade -1 # Rollback ultima
docker compose exec server alembic -c migrations/alembic.ini upgrade head # Via Docker
```
Nota: `env.py` sovrascrive la URL di alembic.ini con quella da `.env` (`settings.database_url`). `script_location = %(here)s` usa path relativo.
### Test
```bash
@@ -76,24 +78,67 @@ Browser/Tablet → Nginx (:80/443) → Flask Client (:5000) → APIClient → Fa
Il client Flask è un frontend server-side che comunica col backend via REST API. Ogni richiesta dal client al server include l'header `X-API-Key` per autenticazione.
### Server (FastAPI) — `server/`
- **main.py**: entry point, registra middleware (RateLimit → SecurityHeaders → CORS → AccessLog) e 10 router
- **config.py**: `Settings` Pydantic che legge da `.env`
- **database.py**: SQLAlchemy 2.0 async engine con `AsyncSession`, pool 10+20 overflow
- **routers/**: auth, recipes, tasks, measurements, statistics, reports, files, users, settings, setup
- **main.py**: entry point, lifespan async (`@asynccontextmanager`), registra middleware e 10 router. Health: `GET /api/health`
- **config.py**: `Settings` (pydantic_settings.BaseSettings), legge da `../.env`. Rate limits: login 5/min, general 100/min
- **database.py**: SQLAlchemy 2.0 async engine con `AsyncSession`, pool 10+20 overflow, `pool_recycle=3600`, `expire_on_commit=False`
- **routers/**: auth, users, recipes, tasks, measurements, files, settings, statistics, reports, setup
- **services/**: recipe_service (versioning copy-on-write), measurement_service (calcolo pass/fail), spc_service (Cp/Cpk/control chart, puro stdlib senza numpy), report_service (WeasyPrint PDF + Kaleido SVG), auth_service (bcrypt + API key)
- **middleware/**: api_key.py (auth dependency), rate_limit.py (sliding window per-IP), security_headers.py (CSP, HSTS), logging.py (audit trail async su DB)
- **models/**: User, Recipe, RecipeVersion, RecipeTask, RecipeSubtask, Measurement, AccessLog, SystemSetting
- **middleware/**: api_key.py (auth dependency), rate_limit.py (sliding window 60s per-IP, in-memory dicts), security_headers.py (CSP con `unsafe-eval` per Plotly.js, HSTS solo con SSL), logging.py (audit trail async su DB, esclude /api/health, /docs, /openapi.json)
- **models/**: User, Recipe, RecipeVersion, RecipeTask, RecipeSubtask, Measurement, AccessLog, SystemSetting, RecipeVersionAudit
- **schemas/**: Pydantic v2 per validazione I/O API
- **migrations/**: Alembic con alembic.ini nella directory migrations
- **tests/**: pytest + pytest-asyncio, database SQLite in-memory (aiosqlite), fixture pre-popolate (admin_user, maker_user, etc.), WeasyPrint mockato
- **migrations/**: Alembic con `alembic.ini` e `env.py` nella directory `server/migrations/`
- **tests/**: pytest + pytest-asyncio, SQLite in-memory (`sqlite+aiosqlite://`, StaticPool), WeasyPrint mockato via `sys.modules`, rate limit reset tra test
### Client (Flask) — `client/`
- **app.py**: factory pattern `create_app()`, CSRF protection, Babel i18n
- **app.py**: factory pattern `create_app()`, CSRF (`WTF_CSRF_TIME_LIMIT=3600`), Babel i18n (`default_locale="it"`)
- **blueprints/**: auth (login/logout/session), maker (editor ricette con Fabric.js), measure (esecuzione misurazioni), statistics (dashboard SPC con Plotly.js)
- **services/api_client.py**: singleton `APIClient` — wrapper HTTP (get/post/put/delete) con gestione errori normalizzata, timeout 30s, header X-API-Key da session
- **templates/**: Jinja2 con `{{ _('string') }}` per i18n
- **static/js/**: Alpine.js components, Fabric.js editor, locales JSON (it.json, en.json)
- **translations/**: Flask-Babel, cataloghi .po/.mo per IT/EN
- **templates/**: Jinja2 con `{{ _('string') }}` per i18n. Context processor inietta `current_user`, `current_theme`, `current_language`, `company_logo` in tutti i template
- **static/js/**: Alpine.js 3.x (CDN), Fabric.js 5.3.1 (CDN), locales JSON (it.json, en.json), alpine-init.js (theme store)
- **translations/**: Flask-Babel, cataloghi .po/.mo per IT/EN. Locale selector: `session["language"]` → Accept-Language → `"it"`
- **config.py**: `PERMANENT_SESSION_LIFETIME=28800` (8h), cookie secure in produzione, `BABEL_DEFAULT_TIMEZONE="Europe/Rome"`
## Template Structure (`client/templates/base.html`)
Ordine blocchi in `base.html`:
```
<head> → {% block extra_head %} (CSS/meta aggiuntivi)
<body> → {% block content %} (contenuto pagina)
→ {% block extra_js %} (script componenti — PRIMA di Alpine)
→ <script defer alpine.js> (Alpine carica PER ULTIMO)
```
`alpine-init.js` (senza defer) carica in `<head>` per registrare `Alpine.store('theme')` prima del paint. I componenti Alpine definiti in `{% block extra_js %}` sono disponibili quando Alpine si inizializza.
Temi: CSS variables in `themes.css`, toggle dark mode via `<html :class="{ 'dark': $store.theme.dark }">`.
## Alpine.js + Jinja2: Regola di Escaping Critica
**MAI** usare `|tojson` direttamente in attributi `x-data="..."` — le `"` del JSON terminano l'attributo HTML.
Pattern corretto:
```html
<script>window.__myData = {{ data|tojson }};</script>
<div x-data="myComponent(window.__myData)">
```
`|tojson` dentro `<script>` è sempre sicuro (nessun contesto attributo HTML).
Per selettori CSS in `x-data`: usare `meta[name=csrf-token]` senza virgolette interne.
## Fabric.js Annotation Editor (`client/static/js/annotation-editor.js`)
Editor annotazioni su disegni tecnici (Fabric.js 5.3.1, ~1200 righe). Pattern critici:
- **Canvas = immagine**: `_fitCanvasToImage()` dimensiona il canvas esattamente come l'immagine scalata (zero spazio vuoto). MAI usare aspect ratio fisso
- **Oggetti MOVE-ONLY**: `hasControls: false`, `lockRotation/lockScaling: true`
- **enableRetinaScaling: false**: elimina mismatch coordinate DPR
- **getPointer override**: quello built-in di Fabric è rotto su retina. Override BCR-based con `canvas.width/rect.width`
- **Caricamento annotations concatenato all'immagine**: pattern `_pendingAnnotations` — salva JSON, carica nel callback dell'immagine. Mai caricare prima che l'immagine sia impostata
- **Arrow endpoint tracking**: aggiorna `arrowX1/Y1/X2/Y2` in `object:modified` con delta da `e.transform.original`
- **Coordinate scaling on load**: `coordScale = currentImageScale / savedImageScale`
JSON annotations v2: `{ version, width, height, imageScale, objects[] }`. Il campo `imageScale` (= `backgroundImage.scaleX`) è fondamentale per mappare coordinate tra editor e viewer a risoluzioni diverse.
Storage: `RecipeTask.annotations_json` (colonna JSON), `RecipeTask.file_path`, `RecipeTask.file_type` (`"image"` | `"pdf"`).
## Pattern Critici
@@ -122,6 +167,30 @@ Combinabili (JSON array): **Maker** (crea ricette), **MeasurementTec** (esegue m
### File Storage
Upload in `uploads/{recipe_id}/{version_id}/`. Tipi ammessi: JPEG, PNG, GIF, WebP, PDF. Limite 50MB. Thumbnail auto-generate per immagini (Pillow). Sanitizzazione filename.
## Test Infrastructure
### Server (`server/tests/conftest.py`)
- SQLite in-memory `sqlite+aiosqlite://` con `StaticPool` (singola connessione condivisa tra fixture, app e test)
- WeasyPrint mockato prima di qualsiasi import server: `sys.modules["weasyprint"] = MagicMock()`
- Rate limit buckets resettati tra test (walk middleware stack → clear dicts)
- Fixture utenti pre-popolati: `admin_user`, `maker_user`, `measurement_tec_user`, `metrologist_user` con API key
- Helper: `create_test_recipe(session, user_id)` → recipe con v1 current, 1 task, 1 subtask
- Helper: `auth_headers(user)``{"X-API-Key": user.api_key}`
- Client httpx: `AsyncClient` con `ASGITransport(app=app)`, override di `get_db` dependency
### Client (`client/tests/conftest.py`)
- `api_client` patchato in tutti i 4 blueprint (auth, maker, measure, statistics)
- `logged_in_client` fixture pre-popola session con user dict + api_key
- CSRF disabilitato nei test: `WTF_CSRF_ENABLED=False`
## Docker
Container: `tmflow-mysql`, `tmflow-server`, `tmflow-client`, `tmflow-nginx`. Network: `tmflow-net`. Volumes: `mysql_data`, `upload_data`.
Il server esegue Alembic migrations automaticamente all'avvio (`CMD` include `alembic upgrade head && uvicorn`). Il client compila Babel translations e TailwindCSS nel Dockerfile build. Entrambi usano Python 3.11-slim, Gunicorn con 2 workers.
Nginx: gzip abilitato, `client_max_body_size 50M`, `proxy_read_timeout 120s`, cache statica 7d, security headers duplicati a livello proxy.
## Convenzioni Codice
- Python 3.11+, type hints ovunque
- async/await per tutte le operazioni DB (server)
@@ -138,10 +207,11 @@ Upload in `uploads/{recipe_id}/{version_id}/`. Tipi ammessi: JPEG, PNG, GIF, Web
## Configurazione
Variabili d'ambiente in `.env` (copiare da `.env.example`):
- DB: `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`
- DB: `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`, `DB_ROOT_PASSWORD` (Docker)
- Server: `SERVER_HOST`, `SERVER_PORT`, `SERVER_SECRET_KEY`, `SERVER_CORS_ORIGINS`
- Client: `CLIENT_HOST`, `CLIENT_PORT`, `CLIENT_SECRET_KEY`, `API_SERVER_URL`
- Upload: `UPLOAD_DIR`, `MAX_UPLOAD_SIZE_MB`
- Upload: `UPLOAD_DIR` (default `"uploads"`, relativo a `server/`), `MAX_UPLOAD_SIZE_MB`
- Docker: `NGINX_PORT`, `NGINX_SSL_PORT`
- Setup: `SETUP_PASSWORD` (vuota = endpoint disabilitato)
- SSL: `SSL_CERTFILE`, `SSL_KEYFILE`