Files
TieMeasureFlow/CLAUDE.md
T
Adriano 1a0431366f chore(v2): restructure monorepo to src/ layout with uv
Aligns the repo with the python-project-spec-design.md template chosen
for V2.0.0. Big move, no logic changes. The 3 pre-existing test
failures (test_recipes::test_update_recipe, test_recipes::
test_recipe_versioning, test_tasks::test_reorder_tasks, plus the
client test_save_measurement_proxy) survive unchanged.

Layout changes
- server/        -> src/backend/
- server/middleware/ -> src/backend/api/middleware/
- server/routers/    -> src/backend/api/routers/
- server/models/     -> src/backend/models/orm/
- server/schemas/    -> src/backend/models/api/
- server/uploads/    -> uploads/ (project root, mounted volume)
- server/tests/      -> src/backend/tests/
- client/            -> src/frontend/flask_app/ (Flask kept; React
  deroga is documented in CLAUDE.md, justified by tablet UX, USB
  caliper/barcode workflow and Fabric.js integration)

Tooling
- pyproject.toml: monorepo with [project] core deps and
  optional-dependencies server / client / dev. Replaces both
  server/requirements.txt and client/requirements.txt.
- uv.lock + .python-version (3.11) committed for reproducible builds.
- Dockerfile (root, backend) and Dockerfile.frontend rewritten to use
  uv sync --frozen --no-dev --extra server|client; legacy Dockerfiles
  preserved as Dockerfile.legacy for reference but excluded from build
  context via .dockerignore.
- docker-compose.dev.yml + docker-compose.yml: build context now ".",
  dockerfile pointing to the root files.

Code adjustments forced by the move
- Every "from config|database|models|schemas|services|routers|middleware
  import ..." rewritten to its src.backend.* equivalent (50+ files
  including indented inline imports inside test bodies).
- src/backend/migrations/env.py: insert project root into sys.path so
  alembic can resolve src.backend.* imports regardless of cwd.
- src/backend/config.py: env_file ../../.env (was ../.env), upload_path
  resolves project root via parents[2].
- src/backend/tests/conftest.py + tests: import ... from src.backend.*
  instead of bare names; old per-directory pytest.ini files removed in
  favor of root pyproject.toml [tool.pytest.ini_options].
- .gitignore: uploads/ at root, src/frontend/flask_app/static/css/
  tailwind.css path; .dockerignore tightened.
- CLAUDE.md: rewrote sections "Layout del repository", "Comandi di
  Sviluppo", "Database & Migrations", "Test", "i18n", and all path
  references throughout the architecture sections.

Verified
- uv lock resolves 77 packages; uv sync --extra server --extra client
  --extra dev installs cleanly.
- uv run pytest: 171 passed, 4 pre-existing failures.
- uv run alembic -c src/backend/migrations/alembic.ini check loads
  config and metadata (errors only on the absent local MySQL).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 12:26:47 +02:00

19 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 backend FastAPI (porta 8000) e frontend Flask (porta 5000), orchestrati con Docker Compose + Nginx reverse proxy + MySQL 8.0.

Layout del repository (V2.0.0)

A partire dalla migrazione V2.0.0 (struttura conforme alla spec python-project-spec-design.md):

TieMeasureFlow/
├── pyproject.toml              # Dipendenze monorepo (uv)
├── uv.lock                     # Lock file riproducibile
├── .python-version             # 3.11
├── Dockerfile                  # Backend (uv + uvicorn)
├── Dockerfile.frontend         # Frontend (uv + gunicorn + Tailwind + Babel)
├── docker-compose.dev.yml      # Dev (Nginx)
├── docker-compose.yml          # Prod (Traefik + SSL)
├── nginx/
├── uploads/                    # Volume montato in /app/uploads
├── docs/
└── src/
    ├── backend/
    │   ├── main.py             # Entry FastAPI
    │   ├── config.py
    │   ├── database.py
    │   ├── api/
    │   │   ├── routers/        # 11 router REST
    │   │   └── middleware/     # api_key, rate_limit, security_headers, logging
    │   ├── models/
    │   │   ├── orm/            # SQLAlchemy
    │   │   └── api/            # Pydantic schemas
    │   ├── services/           # Logica business
    │   ├── migrations/         # Alembic
    │   ├── templates/          # Setup page
    │   └── tests/              # pytest
    └── frontend/
        └── flask_app/          # Flask + Jinja2 + Alpine.js (deroga vs spec React,
            ├── app.py          # giustificata: tablet UX server-side, USB calipers,
            ├── config.py       # workflow operatore con Fabric.js)
            ├── blueprints/
            ├── services/
            ├── templates/
            ├── static/
            ├── translations/
            └── tests/

Dipendenze gestite con uv (no requirements.txt):

  • [project] dependencies = core condivisi (pydantic, dotenv)
  • [project.optional-dependencies] server = backend FastAPI
  • [project.optional-dependencies] client = frontend Flask
  • [project.optional-dependencies] dev = pytest, httpx, aiosqlite

Comandi di Sviluppo

Setup iniziale

cp .env.example .env
uv sync --extra server --extra client --extra dev   # installa tutto

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)

# Backend (terminale 1)
uv run uvicorn src.backend.main:app --reload --host 0.0.0.0 --port 8000

# Frontend (terminale 2) — gunicorn richiede cwd interna per app:create_app()
cd src/frontend/flask_app && uv run --project ../../.. gunicorn --bind 0.0.0.0:5000 app:create_app()

# TailwindCSS watch (terminale 3)
cd src/frontend/flask_app && npx tailwindcss -i static/css/input.css -o static/css/tailwind.css --watch

Database & Migrations

# alembic.ini in src/backend/migrations/, serve il flag -c
uv run alembic -c src/backend/migrations/alembic.ini upgrade head                              # Applica migrazioni
uv run alembic -c src/backend/migrations/alembic.ini revision --autogenerate -m "descrizione"  # Genera
uv run alembic -c src/backend/migrations/alembic.ini downgrade -1                              # Rollback
docker compose exec server uv run alembic -c src/backend/migrations/alembic.ini upgrade head   # Via Docker

Nota: env.py aggiunge la project root a sys.path, sovrascrive la URL di alembic.ini con quella da .env (settings.database_url). script_location = %(here)s usa path relativo.

Test

# Tutti i test (backend + frontend)
uv run pytest

# Solo backend (SQLite in-memory via aiosqlite, no MySQL richiesto)
uv run pytest src/backend/tests/
uv run pytest src/backend/tests/test_auth.py
uv run pytest src/backend/tests/test_auth.py::test_login_success
uv run pytest --cov src/backend

# Solo frontend
uv run pytest src/frontend/flask_app/tests/

Gestione dipendenze (uv)

uv add <pacchetto>                          # core (entrambi)
uv add --optional server <pacchetto>        # solo backend
uv add --optional client <pacchetto>        # solo frontend
uv add --optional dev <pacchetto>           # solo dev/test
uv sync --extra server --extra client --extra dev  # reinstalla
uv lock                                     # rigenera uv.lock

i18n (Traduzioni)

# Estrai stringhe
cd src/frontend/flask_app && uv run pybabel extract -F babel.cfg -k _ -o translations/messages.pot .

# Aggiorna catalogo
cd src/frontend/flask_app && uv run pybabel update -i translations/messages.pot -d translations

# Compila .po → .mo
cd src/frontend/flask_app && uv run pybabel compile -d translations
# oppure
cd src/frontend/flask_app && uv run 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) — src/backend/

  • 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): AccessLogMiddleware → CORSMiddleware → SecurityHeadersMiddleware → RateLimitMiddleware. Nota: add_middleware() in Starlette wrappa l'app, quindi l'ultimo aggiunto (AccessLog) è il più esterno. Il commento in main.py dice "outermost" per RateLimit ma è fuorviante. api_key.py (auth dependency get_current_user()), 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, /redoc)
  • models/: User (include email, language_pref, theme_pref), Recipe (image_path per preview), RecipeVersion, RecipeTask, RecipeSubtask (image_path per immagine specifica), Measurement (synced_to_csv, input_method), AccessLog, SystemSetting, RecipeVersionAudit
  • schemas/: Pydantic v2 per validazione I/O API
  • migrations/: Alembic con alembic.ini e env.py nella directory src/backend/migrations/
  • tests/: pytest + pytest-asyncio, SQLite in-memory (sqlite+aiosqlite://, StaticPool), WeasyPrint mockato via sys.modules, rate limit reset tra test

Client (Flask) — src/frontend/flask_app/

  • 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 inietta current_user, current_theme, current_language, company_logo, languages in tutti i template. Filtro custom tojson_attr per JSON sicuro in attributi HTML (escapa "&#34;)
  • static/js/: Alpine.js 3.x (CDN), Fabric.js 5.3.1 (CDN) + fabric-debug.js (copia locale 5.3.0 per debug), 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 (src/frontend/flask_app/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 "&#34; 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 (src/frontend/flask_app/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, ACTIVATE, RETIRE). Logica in src/backend/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 src/backend/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 src/backend/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. 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 (src/backend/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 (src/frontend/flask_app/tests/conftest.py)

  • api_client patchato in 4 blueprint (auth, maker, measure, statistics). admin NON è patchato — i test admin devono gestire il mock manualmente
  • logged_in_client fixture pre-popola session con api_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 esterna root_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 a src/backend/), 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) → primary in tailwind.config.js (palette completa 50900)
  • Colore secondario: #64748B (grigio acciaio) → steel in tailwind.config.js (palette completa 50900)
  • 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 via Alpine.store('theme') con localStorage key tmf-theme