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:
@@ -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`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user