Files
TieMeasureFlow/CLAUDE.md
T
Adriano 9502388064 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>
2026-02-16 21:25:16 +01:00

223 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Panoramica
TieMeasureFlow by Tielogic - Sistema di gestione task per misurazioni con calibro manuale.
Monorepo con **server FastAPI** (backend API, porta 8000) e **client Flask** (frontend tablet, porta 5000), orchestrati con Docker Compose + Nginx reverse proxy + MySQL 8.0.
## Comandi di Sviluppo
### Avvio servizi (Docker)
```bash
docker compose up -d # Avvia tutto
docker compose logs -f server # Log server
docker compose logs -f client # Log client
docker compose ps # Stato servizi
```
### Avvio manuale (senza Docker)
```bash
# Server (terminale 1)
cd server && uvicorn main:app --reload --host 0.0.0.0 --port 8000
# Client (terminale 2)
cd client && flask run --host 0.0.0.0 --port 5000
# TailwindCSS watch (terminale 3)
cd client && npx tailwindcss -i static/css/input.css -o static/css/tailwind.css --watch
```
### Database & Migrations
```bash
# 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
# Server (usa SQLite in-memory via aiosqlite, no MySQL richiesto)
cd server && pytest # Tutti i test
cd server && pytest tests/test_auth.py # Singolo modulo
cd server && pytest tests/test_auth.py::test_login_success # Singolo test
cd server && pytest --cov # Con copertura
# Client
cd client && pytest
cd client && pytest tests/test_auth.py
```
### i18n (Traduzioni)
```bash
# Estrai stringhe
cd client && pybabel extract -F babel.cfg -k _ -o translations/messages.pot .
# Aggiorna catalogo
cd client && pybabel update -i translations/messages.pot -d translations
# Compila .po → .mo
cd client && pybabel compile -d translations
# oppure
cd client && python compile_translations.py
```
### Setup iniziale
Dopo primo avvio, aprire `http://localhost/api/setup` (o `:8000/api/setup` senza Nginx) con la SETUP_PASSWORD dal `.env` per: inizializzare DB, creare admin, caricare dati demo.
## Architettura
### Flusso richieste
```
Browser/Tablet → Nginx (:80/443) → Flask Client (:5000) → APIClient → FastAPI Server (:8000) → MySQL
```
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, 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 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` 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 (`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. 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
### Recipe Versioning (Copy-on-Write)
Le ricette usano un versioning immutabile: ogni modifica crea una nuova `RecipeVersion` con deep-copy di tasks e subtasks. Le misurazioni restano sempre legate alla versione originale. La versione corrente ha `is_current=True`, le precedenti `False`. Audit trail in `recipe_version_audit` (CREATE, UPDATE, RETIRE). Logica in `server/services/recipe_service.py`.
### Calcolo Pass/Fail
Ogni subtask ha 4 limiti di tolleranza: UTL (upper tolerance), UWL (upper warning), LWL (lower warning), LTL (lower tolerance) più un valore nominale. Il calcolo in `server/services/measurement_service.py`:
- Fuori UTL/LTL → **fail**
- Fuori UWL/LWL ma dentro UTL/LTL → **warning**
- Dentro UWL/LWL → **pass**
### SPC (Statistical Process Control)
Calcoli in `server/services/spc_service.py` usando solo `math` e `statistics` stdlib (no numpy/scipy): summary (conteggi pass/warning/fail), capability (Cp, Cpk, Pp, Ppk), control chart (UCL/LCL = mean ± 3σ), histogram (20 bin + curva normale).
### Autenticazione
1. Login con username/password → server ritorna `api_key` (64 char random)
2. Client salva api_key in Flask session
3. Ogni richiesta API include header `X-API-Key`
4. Middleware `get_current_user()` autentica, `require_role()` autorizza
5. Dependency pre-built: `require_maker`, `require_measurement_tec`, `require_metrologist`, `require_admin_user`
### Ruoli Utente
Combinabili (JSON array): **Maker** (crea ricette), **MeasurementTec** (esegue misurazioni), **Metrologist** (dashboard SPC). Flag `is_admin` separato.
### 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)
- Pydantic v2 per validazione I/O API
- Nomi variabili e commenti in inglese, UI strings in IT/EN via i18n
- Import ordinati: stdlib → third-party → local, nessun wildcard
- DB queries: `selectinload()` per eager loading, `scalar_one_or_none()` per single-row
## Dipendenze Chiave
- **Server**: fastapi, uvicorn, sqlalchemy[asyncio], asyncmy, alembic, pydantic, bcrypt, pillow, plotly, kaleido, weasyprint, jinja2
- **Client**: flask, flask-babel, flask-wtf, requests
- **Test server**: pytest, pytest-asyncio, httpx, aiosqlite (SQLite in-memory per test)
- **Test client**: pytest, coverage
## Configurazione
Variabili d'ambiente in `.env` (copiare da `.env.example`):
- 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` (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`
## Brand
- Colore primario: #2563EB (blu industriale)
- Colore secondario: #64748B (grigio acciaio)
- Font UI: Inter
- Font numeri: JetBrains Mono