945c873d38
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
238 lines
14 KiB
Markdown
238 lines
14 KiB
Markdown
# 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. Il task router (`tasks.py`) usa URL pattern misti: `/api/recipes/{id}/tasks` (list/create), `/api/tasks/{id}` (get/update/delete), `/api/tasks/reorder`, `/api/tasks/{id}/subtasks`, `/api/subtasks/{id}`
|
||
- **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). Componenti specializzati: `numpad.js` (input numerico touch con rilevamento burst USB HID), `caliper.js` (monitor calibro USB passivo), `barcode.js` (scanner QR/barcode con camera via html5-qrcode), `csv-export.js` (export CSV con locale italiano: `;` separatore, `,` decimale), `spc-charts.js` (grafici SPC con Plotly.js), `annotation-editor.js` / `annotation-viewer.js` (editor/viewer annotazioni Fabric.js)
|
||
- **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 (Conditional Copy-on-Write)
|
||
Le ricette usano un versioning condizionale. L'endpoint `PUT /api/recipes/{id}` decide la strategia:
|
||
- **Se la versione corrente ha measurements** → copy-on-write: crea nuova `RecipeVersion` con deep-copy di tasks e subtasks. Le misurazioni restano legate alla versione originale.
|
||
- **Se la versione corrente NON ha measurements** → update in-place via `update_current_version()` (nessuna nuova versione creata).
|
||
|
||
La stessa logica si applica nel task router: aggiungere un task a una ricetta con measurements crea una nuova versione.
|
||
|
||
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.
|
||
|
||
### Measurement Workflow (Client)
|
||
Flusso completo per l'operatore MeasurementTec:
|
||
1. **select_recipe** (`/select`) — scelta ricetta con ricerca testo e barcode scanner. Supporta auto-fill da query params (`?recipe=&lot=&serial=`)
|
||
2. **task_list** (`/tasks/<recipe_id>`) — lista task della versione corrente. Lotto e seriale persistono in Flask session
|
||
3. **task_execute** (`/execute/<task_id>`) — esecuzione misurazioni con numpad touch, input USB calibro (burst detection), annotation viewer. Auto-advance al task successivo. Salvataggio AJAX via `POST /save-measurement` (proxy Flask → FastAPI)
|
||
4. **task_complete** (`/complete/<recipe_id>`) — riepilogo con tutte le misurazioni, deviazioni calcolate client-side se mancanti
|
||
|
||
Il blueprint measure include un **file proxy** (`/measure/api/files/<path>`) perché il browser non può inviare l'header `X-API-Key` direttamente al server FastAPI.
|
||
|
||
### 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
|