Correct client conftest patching (4 blueprints, not 5 - admin excluded), add middleware stack order with CORSMiddleware, fix RecipeVersionAudit enum (add ACTIVATE), clarify server/client Docker workers (uvicorn vs gunicorn), document tojson_attr filter, measure AJAX routes, Tailwind custom colors, and dark mode config. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
16 KiB
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)
docker compose -f docker-compose.dev.yml up -d # Sviluppo (Nginx, porta 80)
docker compose up -d # Produzione (Traefik, SSL)
docker compose logs -f server # Log server
docker compose logs -f client # Log client
docker compose ps # Stato servizi
Avvio manuale (senza Docker)
# 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
# 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
# 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)
# 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/: Stack order (outermost→innermost): RateLimitMiddleware → SecurityHeadersMiddleware → CORSMiddleware → AccessLogMiddleware. api_key.py (auth dependency
get_current_user()), rate_limit.py (sliding window 60s per-IP, in-memory dicts), security_headers.py (CSP conunsafe-evalper Plotly.js, HSTS solo con SSL), logging.py (audit trail async su DB, esclude /api/health, /docs, /openapi.json, /redoc) - models/: User (include
email,language_pref,theme_pref), Recipe (image_pathper preview), RecipeVersion, RecipeTask, RecipeSubtask (image_pathper immagine specifica), Measurement (synced_to_csv,input_method), AccessLog, SystemSetting, RecipeVersionAudit - schemas/: Pydantic v2 per validazione I/O API
- migrations/: Alembic con
alembic.inieenv.pynella directoryserver/migrations/ - tests/: pytest + pytest-asyncio, SQLite in-memory (
sqlite+aiosqlite://, StaticPool), WeasyPrint mockato viasys.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), admin (gestione utenti CRUD, cambio password, toggle attivo — solo
is_admin) - 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 iniettacurrent_user,current_theme,current_language,company_logo,languagesin tutti i template. Filtro customtojson_attrper JSON sicuro in attributi HTML (escapa"→") - 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:
<script>window.__myData = {{ data|tojson }};</script>
<div x-data="myComponent(window.__myData)">
|tojson dentro <script> è sempre sicuro (nessun contesto attributo HTML).
In alternativa, usare il filtro custom |tojson_attr (registrato in app.py) che escapa " → " per l'uso diretto in attributi 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/Y2inobject:modifiedcon delta dae.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
RecipeVersioncon 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, ACTIVATE, 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
- Login con username/password → server ritorna
api_key(64 char random) - Client salva api_key in Flask session
- Ogni richiesta API include header
X-API-Key - Middleware
get_current_user()autentica,require_role()autorizza - 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:
- select_recipe (
/select) — scelta ricetta con ricerca testo e barcode scanner. Supporta auto-fill da query params (?recipe=&lot=&serial=) - task_list (
/tasks/<recipe_id>) — lista task della versione corrente. Lotto e seriale persistono in Flask session - task_execute (
/execute/<task_id>) — esecuzione misurazioni con numpad touch, input USB calibro (burst detection), annotation viewer. Auto-advance al task successivo. Salvataggio AJAX viaPOST /save-measurement(proxy Flask → FastAPI) - 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. Route AJAX aggiuntive: POST /measure/lookup-barcode (ricerca ricetta da barcode), POST /measure/save-traceability (salva lotto/seriale in session).
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://conStaticPool(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_usercon 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:
AsyncClientconASGITransport(app=app), override diget_dbdependency
Client (client/tests/conftest.py)
api_clientpatchato in 4 blueprint (auth, maker, measure, statistics). admin NON è patchato — i test admin devono gestire il mock manualmentelogged_in_clientfixture pre-popola session conapi_key,user_id,language,theme+ user dict- CSRF disabilitato nei test:
WTF_CSRF_ENABLED=False
Docker
Due file compose:
docker-compose.dev.yml: sviluppo locale con Nginx reverse proxy. 4 servizi: mysql, server, client, nginx. Porte${NGINX_PORT:-80}e${NGINX_SSL_PORT:-443}.docker-compose.yml: produzione con Traefik (SSL auto via Let's Encrypt). 3 servizi: mysql, server, client. Richiede rete esternaroot_default(Traefik).
Container: tmflow-mysql, tmflow-server, tmflow-client (+tmflow-nginx solo dev). 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. Server: uvicorn 2 workers. Client: gunicorn 2 workers (installato inline nel Dockerfile, non in requirements.txt).
Nginx (dev): 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 aserver/),MAX_UPLOAD_SIZE_MB - Docker:
NGINX_PORT,NGINX_SSL_PORT - Setup:
SETUP_PASSWORD(vuota = endpoint disabilitato) - SSL:
SSL_CERTFILE,SSL_KEYFILE
Brand & Tailwind Custom Colors
- Colore primario: #2563EB (blu industriale) →
primaryin tailwind.config.js - Colore secondario: #64748B (grigio acciaio) →
steelin tailwind.config.js - Colori misurazioni:
measure-pass(#059669),measure-warning(#D97706),measure-fail(#DC2626) - Font UI: Inter
- Font numeri: JetBrains Mono
- Dark mode:
darkMode: 'class'in tailwind.config.js, toggle viaAlpine.store('theme')con localStorage keytmf-theme