Files
TieMeasureFlow/CLAUDE.md
T

14 KiB
Raw Blame History

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 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)

# 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/: 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:

<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